From 3128a8aad8ac0887112efc9f6ab881d1cff4db1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 11:14:49 +0100 Subject: [PATCH 001/193] avalon_apps added to pype just to separate them from pype-setup --- pype/avalon_apps/__init__.py | 7 ++++ pype/avalon_apps/avalon_app.py | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 pype/avalon_apps/__init__.py create mode 100644 pype/avalon_apps/avalon_app.py diff --git a/pype/avalon_apps/__init__.py b/pype/avalon_apps/__init__.py new file mode 100644 index 0000000000..a578d03ccc --- /dev/null +++ b/pype/avalon_apps/__init__.py @@ -0,0 +1,7 @@ +from .avalon_app import AvalonApps + + +def tray_init(tray_widget, main_widget): + av_apps = AvalonApps(main_widget, tray_widget) + av_apps.tray_menu(tray_widget.menu) + return av_apps diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py new file mode 100644 index 0000000000..303f5c9f03 --- /dev/null +++ b/pype/avalon_apps/avalon_app.py @@ -0,0 +1,69 @@ +import os +import argparse +from Qt import QtGui, QtWidgets +from avalon.tools import libraryloader +from pypeapp.api import Logger +from avalon import io +from launcher import launcher_widget, lib as launcher_lib + + +class AvalonApps: + def __init__(self, main_parent=None, parent=None): + self.log = Logger().get_logger(__name__) + self.main_parent = main_parent + self.parent = parent + self.app_launcher = None + + # Definition of Tray menu + def tray_menu(self, parent_menu=None): + # Actions + if parent_menu is None: + if self.parent is None: + self.log.warning('Parent menu is not set') + return + elif self.parent.hasattr('menu'): + parent_menu = self.parent.menu + else: + self.log.warning('Parent menu is not set') + return + + avalon_launcher_icon = launcher_lib.resource("icon", "main.png") + aShowLauncher = QtWidgets.QAction( + QtGui.QIcon(avalon_launcher_icon), "&Launcher", parent_menu + ) + + aLibraryLoader = QtWidgets.QAction("&Library", parent_menu) + + parent_menu.addAction(aShowLauncher) + parent_menu.addAction(aLibraryLoader) + + aShowLauncher.triggered.connect(self.show_launcher) + aLibraryLoader.triggered.connect(self.show_library_loader) + + return + + def show_launcher(self): + # if app_launcher don't exist create it/otherwise only show main window + if self.app_launcher is None: + parser = argparse.ArgumentParser() + parser.add_argument("--demo", action="store_true") + parser.add_argument( + "--root", default=os.environ["AVALON_PROJECTS"] + ) + kwargs = parser.parse_args() + + root = kwargs.root + root = os.path.realpath(root) + io.install() + APP_PATH = launcher_lib.resource("qml", "main.qml") + self.app_launcher = launcher_widget.Launcher(root, APP_PATH) + + self.app_launcher.window.show() + + def show_library_loader(self): + libraryloader.show( + parent=self.main_parent, + icon=self.parent.icon, + show_projects=True, + show_libraries=True + ) From c01f5946acc7fd8c1369e4992bbd19441dd4c77e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 11:15:03 +0100 Subject: [PATCH 002/193] added tray_init to ftrack module --- pype/ftrack/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index bf18979e91..dd1be1796f 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -1,2 +1,9 @@ from .lib import * from .ftrack_server import * +from .ftrack_run import FtrackRunner + + +def tray_init(tray_widget, main_widget): + ftrack = FtrackRunner(main_widget, tray_widget) + main_widget.menu.addMenu(ftrack.trayMenu(tray_widget.menu)) + ftrack.validate() From ed0dc701302e4145bba018a723dd693399f0af3d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2019 14:56:55 +0100 Subject: [PATCH 003/193] enhanced tray_init in ftrack and avalon apps --- pype/avalon_apps/__init__.py | 3 ++- pype/avalon_apps/avalon_app.py | 14 ++++---------- pype/ftrack/__init__.py | 6 ++++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/pype/avalon_apps/__init__.py b/pype/avalon_apps/__init__.py index a578d03ccc..1d7c3c8a81 100644 --- a/pype/avalon_apps/__init__.py +++ b/pype/avalon_apps/__init__.py @@ -1,7 +1,8 @@ from .avalon_app import AvalonApps -def tray_init(tray_widget, main_widget): +def tray_init(tray_widget, main_widget, parent_menu): av_apps = AvalonApps(main_widget, tray_widget) av_apps.tray_menu(tray_widget.menu) + return av_apps diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index 303f5c9f03..072d7fbb3f 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -27,20 +27,15 @@ class AvalonApps: self.log.warning('Parent menu is not set') return - avalon_launcher_icon = launcher_lib.resource("icon", "main.png") - aShowLauncher = QtWidgets.QAction( - QtGui.QIcon(avalon_launcher_icon), "&Launcher", parent_menu - ) - + icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) + aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) aLibraryLoader = QtWidgets.QAction("&Library", parent_menu) - parent_menu.addAction(aShowLauncher) - parent_menu.addAction(aLibraryLoader) - aShowLauncher.triggered.connect(self.show_launcher) aLibraryLoader.triggered.connect(self.show_library_loader) - return + parent_menu.addAction(aShowLauncher) + parent_menu.addAction(aLibraryLoader) def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window @@ -57,7 +52,6 @@ class AvalonApps: io.install() APP_PATH = launcher_lib.resource("qml", "main.qml") self.app_launcher = launcher_widget.Launcher(root, APP_PATH) - self.app_launcher.window.show() def show_library_loader(self): diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index dd1be1796f..cdaacca68c 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -3,7 +3,9 @@ from .ftrack_server import * from .ftrack_run import FtrackRunner -def tray_init(tray_widget, main_widget): +def tray_init(tray_widget, main_widget, parent_menu): ftrack = FtrackRunner(main_widget, tray_widget) - main_widget.menu.addMenu(ftrack.trayMenu(tray_widget.menu)) + main_widget.menu.addMenu(ftrack.trayMenu(parent_menu)) ftrack.validate() + + return ftrack From d85a3d9f6764849535b612144f1471b9b2460730 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:13:04 +0100 Subject: [PATCH 004/193] added idle manager as service that checks users idle time --- pype/services/__init__.py | 0 pype/services/idle_manager/__init__.py | 7 ++ pype/services/idle_manager/idle_manager.py | 97 ++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 pype/services/__init__.py create mode 100644 pype/services/idle_manager/__init__.py create mode 100644 pype/services/idle_manager/idle_manager.py diff --git a/pype/services/__init__.py b/pype/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/services/idle_manager/__init__.py b/pype/services/idle_manager/__init__.py new file mode 100644 index 0000000000..7c07d3ebee --- /dev/null +++ b/pype/services/idle_manager/__init__.py @@ -0,0 +1,7 @@ +from .idle_manager import IdleManager + + +def tray_init(tray_widget, main_widget): + manager = IdleManager() + manager.start() + return manager diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py new file mode 100644 index 0000000000..cc6e177916 --- /dev/null +++ b/pype/services/idle_manager/idle_manager.py @@ -0,0 +1,97 @@ +import time +from Qt import QtCore, QtGui, QtWidgets +from pype.vendor.pynput import mouse, keyboard +from pypeapp import Logger + + +class IdleManager(QtCore.QThread): + time_signals = {} + idle_time = 0 + signal_reset_timer = QtCore.Signal() + + def __init__(self): + super(IdleManager, self).__init__() + self.log = Logger().get_logger(self.__class__.__name__) + self.signal_reset_timer.connect(self._reset_time) + self._is_running = False + + def add_time_signal(self, emit_time, signal): + if emit_time not in self.time_signals: + self.time_signals[emit_time] = [] + self.time_signals[emit_time].append(signal) + + @property + def is_running(self): + return self._is_running + + def _reset_time(self): + self.idle_time = 0 + + def stop(self): + self._is_running = False + + def run(self): + self.log.info('IdleManager has started') + self._is_running = True + thread_mouse = MouseThread(self.signal_reset_timer) + thread_mouse.start() + thread_keyboard = KeyboardThread(self.signal_reset_timer) + thread_keyboard.start() + while self._is_running: + self.idle_time += 1 + if self.idle_time in self.time_signals: + for signal in self.time_signals[self.idle_time]: + signal.emit() + time.sleep(1) + + thread_mouse.signal_stop.emit() + thread_mouse.terminate() + thread_mouse.wait() + thread_keyboard.signal_stop.emit() + thread_keyboard.terminate() + thread_keyboard.wait() + self.log.info('IdleManager has stopped') + + +class MouseThread(QtCore.QThread): + signal_stop = QtCore.Signal() + + def __init__(self, signal): + super(MouseThread, self).__init__() + self.signal_stop.connect(self.stop) + self.m_listener = None + + self.signal_reset_timer = signal + + def stop(self): + if self.m_listener is not None: + self.m_listener.stop() + + def on_move(self, posx, posy): + self.signal_reset_timer.emit() + + def run(self): + self.m_listener = mouse.Listener(on_move=self.on_move) + self.m_listener.start() + + +class KeyboardThread(QtCore.QThread): + signal_stop = QtCore.Signal() + + def __init__(self, signal): + super(KeyboardThread, self).__init__() + self.signal_stop.connect(self.stop) + self.k_listener = None + + self.signal_reset_timer = signal + + def stop(self): + if self.k_listener is not None: + self.k_listener.stop() + + def on_press(self, key): + self.signal_reset_timer.emit() + + def run(self): + self.k_listener = keyboard.Listener(on_press=self.on_press) + self.k_listener.start() From de4cb207ebaee18167c05fa78cfccb0c9c44af1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:18:14 +0100 Subject: [PATCH 005/193] added timers manager that cares about all timers --- pype/services/timers_manager/__init__.py | 6 + .../services/timers_manager/timers_manager.py | 135 +++++++++++++++ .../timers_manager/widget_user_idle.py | 155 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 pype/services/timers_manager/__init__.py create mode 100644 pype/services/timers_manager/timers_manager.py create mode 100644 pype/services/timers_manager/widget_user_idle.py diff --git a/pype/services/timers_manager/__init__.py b/pype/services/timers_manager/__init__.py new file mode 100644 index 0000000000..a6c4535f3d --- /dev/null +++ b/pype/services/timers_manager/__init__.py @@ -0,0 +1,6 @@ +from .timers_manager import TimersManager +from .widget_user_idle import WidgetUserIdle + + +def tray_init(tray_widget, main_widget): + return TimersManager(tray_widget, main_widget) diff --git a/pype/services/timers_manager/timers_manager.py b/pype/services/timers_manager/timers_manager.py new file mode 100644 index 0000000000..e6c1c4f18a --- /dev/null +++ b/pype/services/timers_manager/timers_manager.py @@ -0,0 +1,135 @@ +from Qt import QtCore +from .widget_user_idle import WidgetUserIdle +from pypeapp.lib.config import get_presets + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super( + Singleton, cls + ).__call__(*args, **kwargs) + return cls._instances[cls] + + +class TimersManager(metaclass=Singleton): + modules = [] + is_running = False + last_task = None + + def __init__(self, tray_widget, main_widget): + self.tray_widget = tray_widget + self.main_widget = main_widget + self.widget_user_idle = WidgetUserIdle(self) + self.set_signal_times() + + def set_signal_times(self): + timer_info = get_presets()['services']['timers_manager']['timer'] + full_time = int(timer_info['full_time'])*60 + message_time = int(timer_info['message_time'])*60 + self.time_show_message = full_time - message_time + self.time_stop_timer = full_time + + def add_module(self, module): + self.modules.append(module) + + def start_timers(self, data): + ''' + Dictionary "data" should contain: + - project_name(str) - Name of Project + - hierarchy(list/tuple) - list of parents(except project) + - task_type(str) + - task_name(str) + + Example: + - to run timers for task in + 'C001_BackToPast/assets/characters/villian/Lookdev BG' + - input data should contain: + data = { + 'project_name': 'C001_BackToPast', + 'hierarchy': ['assets', 'characters', 'villian'], + 'task_type': 'lookdev', + 'task_name': 'Lookdev BG' + } + ''' + self.last_task = data + for module in self.modules: + module.start_timer_manager(data) + self.is_running = True + + def restart_timers(self): + if self.last_task is not None: + self.start_timers(self.last_task) + + def stop_timers(self): + if self.is_running is False: + return + self.widget_user_idle.bool_not_stopped = False + self.widget_user_idle.refresh_context() + for module in self.modules: + module.stop_timer_manager() + self.is_running = False + + def process_modules(self, modules): + self.s_handler = SignalHandler(self) + + if 'IdleManager' in modules: + self.idle_man = modules['IdleManager'] + # Times when idle is between show widget and stop timers + for num in range(self.time_show_message-1, self.time_stop_timer): + self.idle_man.add_time_signal( + num, + self.s_handler.signal_change_label + ) + # Times when widget is already shown and user restart idle + for num in range(self.time_stop_timer - self.time_show_message): + self.idle_man.add_time_signal( + num, + self.s_handler.signal_change_label + ) + # Time when message is shown + self.idle_man.add_time_signal( + self.time_show_message, + self.s_handler.signal_show_message + ) + # Time when timers are stopped + self.idle_man.add_time_signal( + self.time_stop_timer, self.s_handler.signal_stop_timers + ) + + def change_label(self): + if self.is_running is False: + return + if self.widget_user_idle.bool_is_showed is False: + return + if not hasattr(self, 'idle_man'): + return + + if self.idle_man.idle_time > self.time_show_message: + value = self.time_stop_timer - self.idle_man.idle_time + else: + value = 1 + ( + self.time_stop_timer - + self.time_show_message - + self.idle_man.idle_time + ) + self.widget_user_idle.change_count_widget(value) + + def show_message(self): + if self.is_running is False: + return + if self.widget_user_idle.bool_is_showed is False: + self.widget_user_idle.show() + + +class SignalHandler(QtCore.QObject): + signal_show_message = QtCore.Signal() + signal_change_label = QtCore.Signal() + signal_stop_timers = QtCore.Signal() + def __init__(self, cls): + super().__init__() + self.signal_show_message.connect(cls.show_message) + self.signal_change_label.connect(cls.change_label) + self.signal_stop_timers.connect(cls.stop_timers) diff --git a/pype/services/timers_manager/widget_user_idle.py b/pype/services/timers_manager/widget_user_idle.py new file mode 100644 index 0000000000..b65ffd40ba --- /dev/null +++ b/pype/services/timers_manager/widget_user_idle.py @@ -0,0 +1,155 @@ +from pypeapp import style, Logger +from Qt import QtCore, QtGui, QtWidgets + + +class WidgetUserIdle(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 160 + + def __init__(self, parent): + + super(WidgetUserIdle, self).__init__() + + self.bool_is_showed = False + self.bool_not_stopped = True + + self.parent_widget = parent + self.setWindowIcon(parent.tray_widget.icon) + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint | + QtCore.Qt.WindowMinimizeButtonHint + ) + + self._translate = QtCore.QCoreApplication.translate + + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.refresh_context() + self.setWindowTitle('Pype - Stop timers') + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName('main') + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName('form') + + msg_info = 'You didn\'t work for a long time.' + msg_question = 'Would you like to stop Timers?' + msg_stopped = ( + 'Your Timers were stopped. Do you want to start them again?' + ) + + self.lbl_info = QtWidgets.QLabel(msg_info) + self.lbl_info.setFont(self.font) + self.lbl_info.setTextFormat(QtCore.Qt.RichText) + self.lbl_info.setObjectName("lbl_info") + self.lbl_info.setWordWrap(True) + + self.lbl_question = QtWidgets.QLabel(msg_question) + self.lbl_question.setFont(self.font) + self.lbl_question.setTextFormat(QtCore.Qt.RichText) + self.lbl_question.setObjectName("lbl_question") + self.lbl_question.setWordWrap(True) + + self.lbl_stopped = QtWidgets.QLabel(msg_stopped) + self.lbl_stopped.setFont(self.font) + self.lbl_stopped.setTextFormat(QtCore.Qt.RichText) + self.lbl_stopped.setObjectName("lbl_stopped") + self.lbl_stopped.setWordWrap(True) + + self.lbl_rest_time = QtWidgets.QLabel("") + self.lbl_rest_time.setFont(self.font) + self.lbl_rest_time.setTextFormat(QtCore.Qt.RichText) + self.lbl_rest_time.setObjectName("lbl_rest_time") + self.lbl_rest_time.setWordWrap(True) + self.lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter) + + self.form.addRow(self.lbl_info) + self.form.addRow(self.lbl_question) + self.form.addRow(self.lbl_stopped) + self.form.addRow(self.lbl_rest_time) + + self.group_btn = QtWidgets.QHBoxLayout() + self.group_btn.addStretch(1) + self.group_btn.setObjectName("group_btn") + + self.btn_stop = QtWidgets.QPushButton("Stop timer") + self.btn_stop.setToolTip('Stop\'s All timers') + self.btn_stop.clicked.connect(self.stop_timer) + + self.btn_continue = QtWidgets.QPushButton("Continue") + self.btn_continue.setToolTip('Timer won\'t stop') + self.btn_continue.clicked.connect(self.continue_timer) + + self.btn_close = QtWidgets.QPushButton("Close") + self.btn_close.setToolTip('Close window') + self.btn_close.clicked.connect(self.close_widget) + + self.btn_restart = QtWidgets.QPushButton("Start timers") + self.btn_restart.setToolTip('Timer will be started again') + self.btn_restart.clicked.connect(self.restart_timer) + + self.group_btn.addWidget(self.btn_continue) + self.group_btn.addWidget(self.btn_stop) + self.group_btn.addWidget(self.btn_restart) + self.group_btn.addWidget(self.btn_close) + + self.main.addLayout(self.form) + self.main.addLayout(self.group_btn) + + return self.main + + def refresh_context(self): + self.lbl_question.setVisible(self.bool_not_stopped) + self.lbl_rest_time.setVisible(self.bool_not_stopped) + self.lbl_stopped.setVisible(not self.bool_not_stopped) + + self.btn_continue.setVisible(self.bool_not_stopped) + self.btn_stop.setVisible(self.bool_not_stopped) + self.btn_restart.setVisible(not self.bool_not_stopped) + self.btn_close.setVisible(not self.bool_not_stopped) + + def change_count_widget(self, time): + str_time = str(time) + self.lbl_rest_time.setText(str_time) + + def stop_timer(self): + self.parent_widget.stop_timers() + self.close_widget() + + def restart_timer(self): + self.parent_widget.restart_timers() + self.close_widget() + + def continue_timer(self): + self.close_widget() + + def closeEvent(self, event): + event.ignore() + if self.bool_not_stopped is True: + self.continue_timer() + else: + self.close_widget() + + def close_widget(self): + self.bool_is_showed = False + self.bool_not_stopped = True + self.refresh_context() + self.hide() + + def showEvent(self, event): + self.bool_is_showed = True From b6bcd8d07672600960cd9cbc786ff8bb76abec57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:21:00 +0100 Subject: [PATCH 006/193] ftrack module tray_menu modified so menu is added to tray context --- pype/ftrack/ftrack_run.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index a722f8d3fe..9362db297f 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -123,9 +123,9 @@ class FtrackRunner: log.error("During Killing action server: {0}".format(e)) # Definition of Tray menu - def trayMenu(self, parent): + def tray_menu(self, parent_menu): # Menu for Tray App - self.menu = QtWidgets.QMenu('Ftrack', parent) + self.menu = QtWidgets.QMenu('Ftrack', parent_menu) self.menu.setProperty('submenu', 'on') self.menu.setStyleSheet(style.load_stylesheet()) @@ -162,7 +162,9 @@ class FtrackRunner: self.bool_logged = False self.set_menu_visibility() - return self.menu + parent_menu.addMenu(self.menu) + + self.validate() # Definition of visibility of each menu actions def set_menu_visibility(self): From fc87f65b222bfde41112c7f05e33d8ca0a168ab7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:24:59 +0100 Subject: [PATCH 007/193] removed unused code in ftrack module --- pype/ftrack/ftrack_run.py | 346 -------------------------------------- 1 file changed, 346 deletions(-) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index 9362db297f..5719262ba6 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -210,76 +210,23 @@ class FtrackRunner: except Exception as e: log.error("During Killing Timer event server: {0}".format(e)) - def start_countdown_thread(self): - if self.thread_timer_coundown is None: - self.thread_timer_coundown = CountdownThread(self) - self.thread_timer_coundown.signal_show_question.connect( - self.show_widget_timer - ) - self.thread_timer_coundown.signal_send_time.connect( - self.change_count_widget - ) - self.thread_timer_coundown.signal_stop_timer.connect( - self.timer_stop - ) - self.thread_timer_coundown.start() - def stop_countdown_thread(self): - if self.thread_timer_coundown is not None: - self.thread_timer_coundown.runs = False - self.thread_timer_coundown.terminate() - self.thread_timer_coundown.wait() - self.thread_timer_coundown = None - - def show_widget_timer(self): - self.widget_timer.show() - self.widget_timer.setWindowState(QtCore.Qt.WindowMinimized) - self.widget_timer.setWindowState(QtCore.Qt.WindowActive) - # self.widget_timer.activateWindow() - - def change_count_widget(self, time): - str_time = str(time).replace(".0", "") - self.widget_timer.lbl_rest_time.setText(str_time) - - def timer_started(self): - self.start_countdown_thread() - - def timer_stopped(self): - self.stop_countdown_thread() - - def timer_stop(self): if self.thread_timer is not None: - self.widget_timer.main_context = False - self.widget_timer.refresh_context() - self.thread_timer.signal_stop_timer.emit() - if self.thread_timer_coundown is not None: - self.stop_countdown_thread() - def timer_restart(self): if self.thread_timer is not None: - self.thread_timer.signal_restart_timer.emit() - self.timer_started() - def timer_continue(self): - if self.thread_timer_coundown is not None: - self.thread_timer_coundown.signal_continue_timer.emit() class FtrackEventsThread(QtCore.QThread): # Senders signal_timer_started = QtCore.Signal() signal_timer_stopped = QtCore.Signal() - # Listeners - signal_stop_timer = QtCore.Signal() - signal_restart_timer = QtCore.Signal() def __init__(self, parent): super(FtrackEventsThread, self).__init__() cred = credentials._get_credentials() self.username = cred['username'] - self.signal_stop_timer.connect(self.ftrack_stop_timer) - self.signal_restart_timer.connect(self.ftrack_restart_timer) self.user = None self.last_task = None @@ -298,7 +245,6 @@ class FtrackEventsThread(QtCore.QThread): timer = self.timer_session.query(timer_query).first() if timer is not None: self.last_task = timer['context'] - self.signal_timer_started.emit() self.timer_session.event_hub.wait() @@ -321,7 +267,6 @@ class FtrackEventsThread(QtCore.QThread): self.last_task = timer['context'] if old is None: - self.signal_timer_started.emit() elif new is None: self.signal_timer_stopped.emit() @@ -332,295 +277,4 @@ class FtrackEventsThread(QtCore.QThread): except Exception as e: log.debug("Timer stop had issues: {}".format(e)) - def ftrack_restart_timer(self): - try: - if (self.last_task is not None) and (self.user is not None): - self.user.start_timer(self.last_task) - self.timer_session.commit() - except Exception as e: - log.debug("Timer stop had issues: {}".format(e)) - - -class CountdownThread(QtCore.QThread): - # Senders - signal_show_question = QtCore.Signal() - signal_send_time = QtCore.Signal(object) - signal_stop_timer = QtCore.Signal() - signal_stop_countdown = QtCore.Signal() - # Listeners - signal_reset_timer = QtCore.Signal() - signal_continue_timer = QtCore.Signal() - - def __init__(self, parent): - super(CountdownThread, self).__init__() - - self.runs = True - self.over_line = False - config_data = self.load_timer_values() - self.count_length = config_data['full_time']*60 - self.border_line = config_data['message_time']*60 + 1 - self.reset_count() - self.signal_reset_timer.connect(self.reset_count) - self.signal_continue_timer.connect(self.continue_timer) - - def continue_timer(self): - self.over_line = False - self.reset_count() - - def reset_count(self): - if self.over_line is True: - self.actual = self.border_line - else: - self.actual = self.count_length - - def stop(self): - self.runs = False - - def run(self): - thread_mouse = MouseThread(self) - thread_mouse.start() - thread_keyboard = KeyboardThread(self) - thread_keyboard.start() - while self.runs: - if self.actual == self.border_line: - self.signal_show_question.emit() - self.over_line = True - - if self.actual <= self.border_line: - self.signal_send_time.emit(self.actual) - - time.sleep(1) - self.actual -= 1 - - if self.actual == 0: - self.runs = False - self.signal_stop_timer.emit() - - thread_mouse.signal_stop.emit() - thread_mouse.terminate() - thread_mouse.wait() - thread_keyboard.signal_stop.emit() - thread_keyboard.terminate() - thread_keyboard.wait() - - def load_timer_values(self): - templates = os.environ['PYPE_STUDIO_TEMPLATES'] - path_items = [templates, 'presets', 'ftrack', 'ftrack_config.json'] - filepath = os.path.sep.join(path_items) - data = dict() - try: - with open(filepath) as data_file: - json_dict = json.load(data_file) - data = json_dict['timer'] - except Exception as e: - msg = ( - 'Loading "Ftrack Config file" Failed.' - ' Please check log for more information.' - ' Times are set to default.' - ) - log.warning("{} - {}".format(msg, str(e))) - - data = self.validate_timer_values(data) - - return data - - def validate_timer_values(self, data): - # default values - if 'full_time' not in data: - data['full_time'] = 15 - if 'message_time' not in data: - data['message_time'] = 0.5 - - # minimum values - if data['full_time'] < 2: - data['full_time'] = 2 - # message time is earlier that full time - if data['message_time'] > data['full_time']: - data['message_time'] = data['full_time'] - 0.5 - return data - - -class MouseThread(QtCore.QThread): - signal_stop = QtCore.Signal() - - def __init__(self, parent): - super(MouseThread, self).__init__() - self.parent = parent - self.signal_stop.connect(self.stop) - self.m_listener = None - - def stop(self): - if self.m_listener is not None: - self.m_listener.stop() - - def on_move(self, posx, posy): - self.parent.signal_reset_timer.emit() - - def run(self): - self.m_listener = mouse.Listener(on_move=self.on_move) - self.m_listener.start() - - -class KeyboardThread(QtCore.QThread): - signal_stop = QtCore.Signal() - - def __init__(self, parent): - super(KeyboardThread, self).__init__() - self.parent = parent - self.signal_stop.connect(self.stop) - self.k_listener = None - - def stop(self): - if self.k_listener is not None: - self.k_listener.stop() - - def on_press(self, key): - self.parent.signal_reset_timer.emit() - - def run(self): - self.k_listener = keyboard.Listener(on_press=self.on_press) - self.k_listener.start() - - -class StopTimer(QtWidgets.QWidget): - - SIZE_W = 300 - SIZE_H = 160 - - def __init__(self, parent=None): - - super(StopTimer, self).__init__() - - self.main_context = True - self.parent = parent - self.setWindowIcon(self.parent.parent.icon) - self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMinimizeButtonHint ) - - self._translate = QtCore.QCoreApplication.translate - - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - - self.resize(self.SIZE_W, self.SIZE_H) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) - self.setStyleSheet(style.load_stylesheet()) - - self.setLayout(self._main()) - self.refresh_context() - self.setWindowTitle('Pype - Stop Ftrack timer') - - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName('main') - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName('form') - - msg_info = 'You didn\'t work for a long time.' - msg_question = 'Would you like to stop Ftrack timer?' - msg_stopped = ( - 'Your Ftrack timer was stopped. Do you want to start again?' - ) - - self.lbl_info = QtWidgets.QLabel(msg_info) - self.lbl_info.setFont(self.font) - self.lbl_info.setTextFormat(QtCore.Qt.RichText) - self.lbl_info.setObjectName("lbl_info") - self.lbl_info.setWordWrap(True) - - self.lbl_question = QtWidgets.QLabel(msg_question) - self.lbl_question.setFont(self.font) - self.lbl_question.setTextFormat(QtCore.Qt.RichText) - self.lbl_question.setObjectName("lbl_question") - self.lbl_question.setWordWrap(True) - - self.lbl_stopped = QtWidgets.QLabel(msg_stopped) - self.lbl_stopped.setFont(self.font) - self.lbl_stopped.setTextFormat(QtCore.Qt.RichText) - self.lbl_stopped.setObjectName("lbl_stopped") - self.lbl_stopped.setWordWrap(True) - - self.lbl_rest_time = QtWidgets.QLabel("") - self.lbl_rest_time.setFont(self.font) - self.lbl_rest_time.setTextFormat(QtCore.Qt.RichText) - self.lbl_rest_time.setObjectName("lbl_rest_time") - self.lbl_rest_time.setWordWrap(True) - self.lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter) - - self.form.addRow(self.lbl_info) - self.form.addRow(self.lbl_question) - self.form.addRow(self.lbl_stopped) - self.form.addRow(self.lbl_rest_time) - - self.group_btn = QtWidgets.QHBoxLayout() - self.group_btn.addStretch(1) - self.group_btn.setObjectName("group_btn") - - self.btn_stop = QtWidgets.QPushButton("Stop timer") - self.btn_stop.setToolTip('Stop\'s Ftrack timer') - self.btn_stop.clicked.connect(self.stop_timer) - - self.btn_continue = QtWidgets.QPushButton("Continue") - self.btn_continue.setToolTip('Timer will continue') - self.btn_continue.clicked.connect(self.continue_timer) - - self.btn_close = QtWidgets.QPushButton("Close") - self.btn_close.setToolTip('Close window') - self.btn_close.clicked.connect(self.close_widget) - - self.btn_restart = QtWidgets.QPushButton("Start timer") - self.btn_restart.setToolTip('Timer will be started again') - self.btn_restart.clicked.connect(self.restart_timer) - - self.group_btn.addWidget(self.btn_continue) - self.group_btn.addWidget(self.btn_stop) - self.group_btn.addWidget(self.btn_restart) - self.group_btn.addWidget(self.btn_close) - - self.main.addLayout(self.form) - self.main.addLayout(self.group_btn) - - return self.main - - def refresh_context(self): - self.lbl_question.setVisible(self.main_context) - self.lbl_rest_time.setVisible(self.main_context) - self.lbl_stopped.setVisible(not self.main_context) - - self.btn_continue.setVisible(self.main_context) - self.btn_stop.setVisible(self.main_context) - self.btn_restart.setVisible(not self.main_context) - self.btn_close.setVisible(not self.main_context) - - def stop_timer(self): - self.parent.timer_stop() - self.close_widget() - - def restart_timer(self): - self.parent.timer_restart() - self.close_widget() - - def continue_timer(self): - self.parent.timer_continue() - self.close_widget() - - def closeEvent(self, event): - event.ignore() - if self.main_context is True: - self.continue_timer() - else: - self.close_widget() - - def close_widget(self): - self.main_context = True - self.refresh_context() - self.hide() From c85b9bc3b7d10a08610aa2d11efcae314fa42bfc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:30:13 +0100 Subject: [PATCH 008/193] ftrack events_thread cares about timer --- pype/ftrack/ftrack_run.py | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index 5719262ba6..2bf9da2dbe 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -220,7 +220,7 @@ class FtrackRunner: class FtrackEventsThread(QtCore.QThread): # Senders - signal_timer_started = QtCore.Signal() + signal_timer_started = QtCore.Signal(object) signal_timer_stopped = QtCore.Signal() def __init__(self, parent): @@ -245,9 +245,30 @@ class FtrackEventsThread(QtCore.QThread): timer = self.timer_session.query(timer_query).first() if timer is not None: self.last_task = timer['context'] + self.signal_timer_started.emit( + self.get_data_from_task(self.last_task) + ) self.timer_session.event_hub.wait() + def get_data_from_task(self, task_entity): + data = {} + data['task_name'] = task_entity['name'] + data['task_type'] = task_entity['type']['name'] + data['project_name'] = task_entity['project']['full_name'] + data['hierarchy'] = self.get_parents(task_entity['parent']) + + return data + + def get_parents(self, entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(self.get_parents(entity['parent'])) + output.append(entity['name']) + + return output + def event_handler(self, event): try: if event['data']['entities'][0]['objectTypeId'] != 'timer': @@ -267,6 +288,9 @@ class FtrackEventsThread(QtCore.QThread): self.last_task = timer['context'] if old is None: + self.signal_timer_started.emit( + self.get_data_from_task(self.last_task) + ) elif new is None: self.signal_timer_stopped.emit() @@ -274,7 +298,28 @@ class FtrackEventsThread(QtCore.QThread): try: self.user.stop_timer() self.timer_session.commit() + self.signal_timer_stopped.emit() except Exception as e: log.debug("Timer stop had issues: {}".format(e)) + def ftrack_start_timer(self, input_data): + if self.user is None: + return + if ( + input_data['task_name'] == self.last_task['name'] and + input_data['hierarchy'][-1] == self.last_task['parent']['name'] + ): + return + task_query = ( + 'Task where name is "{task_name}"' + ' and parent.name is "{entity_name}"' + ' and project.full_name is "{project_name}"' + ).format(**input_data) + + task = self.timer_session.query(task_query).one() + self.last_task = task + self.user.start_timer(task) + self.timer_session.commit() + self.signal_timer_started.emit( + self.get_data_from_task(self.last_task) ) From 39a236f5b88ef99987fb871983e48f4f71d0362b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:31:04 +0100 Subject: [PATCH 009/193] ftrack module has stop and start timer methods for timers manager --- pype/ftrack/ftrack_run.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index 2bf9da2dbe..8f2d450bad 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -25,13 +25,9 @@ class FtrackRunner: self.parent = parent self.widget_login = login_dialog.Login_Dialog_ui(self) - self.widget_timer = StopTimer(self) self.action_server = FtrackServer('action') self.thread_action_server = None self.thread_timer = None - self.thread_timer_coundown = None - - # self.signal_start_timer.connect(self.timerStart) self.bool_logged = False self.bool_action_server = False @@ -211,11 +207,21 @@ class FtrackRunner: log.error("During Killing Timer event server: {0}".format(e)) + def start_timer_manager(self, data): if self.thread_timer is not None: + self.thread_timer.ftrack_start_timer(data) + def stop_timer_manager(self): if self.thread_timer is not None: + self.thread_timer.ftrack_stop_timer() + def timer_started(self, data): + if hasattr(self, 'timer_manager'): + self.timer_manager.start_timers(data) + def timer_stopped(self): + if hasattr(self, 'timer_manager'): + self.timer_manager.stop_timers() class FtrackEventsThread(QtCore.QThread): From d29752c5aaccd90249490f18d6a7c889e129fca7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:33:07 +0100 Subject: [PATCH 010/193] added process_modules to ftrack module which looks for TimersManager --- pype/ftrack/ftrack_run.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index 8f2d450bad..d795edb08b 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -206,6 +206,10 @@ class FtrackRunner: except Exception as e: log.error("During Killing Timer event server: {0}".format(e)) + def process_modules(self, modules): + if 'TimersManager' in modules: + self.timer_manager = modules['TimersManager'] + self.timer_manager.add_module(self) def start_timer_manager(self, data): if self.thread_timer is not None: From 30abd7ff5c24bcadd61c755fd663bbc05df26cb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:35:46 +0100 Subject: [PATCH 011/193] fixed imports to 2.0 in pype/ftrack --- pype/api.py | 2 +- .../actions/action_application_loader.py | 4 ++-- pype/ftrack/actions/action_asset_delete.py | 2 +- .../actions/action_client_review_sort.py | 2 +- pype/ftrack/actions/action_component_open.py | 2 +- .../actions/action_create_cust_attrs.py | 24 ++++--------------- pype/ftrack/actions/action_create_folders.py | 2 +- pype/ftrack/actions/action_delete_asset.py | 2 +- .../actions/action_delete_asset_byname.py | 2 +- .../actions/action_delete_unpublished.py | 2 +- pype/ftrack/actions/action_djvview.py | 4 ++-- pype/ftrack/actions/action_job_killer.py | 4 +--- pype/ftrack/actions/action_set_version.py | 2 +- .../actions/action_sync_to_avalon_local.py | 2 +- pype/ftrack/actions/action_test.py | 2 +- pype/ftrack/actions/action_thumbToChildern.py | 2 +- pype/ftrack/actions/action_thumbToParent.py | 2 +- pype/ftrack/actions/event_collect_entities.py | 2 +- pype/ftrack/credentials.py | 1 - pype/ftrack/ftrack_run.py | 16 ++++--------- pype/ftrack/ftrack_server/event_server.py | 4 ++-- pype/ftrack/ftrack_server/event_server_cli.py | 4 ++-- pype/ftrack/ftrack_server/ftrack_server.py | 6 ++--- pype/ftrack/lib/avalon_sync.py | 4 ++-- pype/ftrack/lib/ftrack_base_handler.py | 4 ++-- pype/ftrack/login_dialog.py | 4 ++-- pype/ftrack/login_tools.py | 2 +- pype/templates.py | 5 ++-- 28 files changed, 46 insertions(+), 68 deletions(-) diff --git a/pype/api.py b/pype/api.py index 747ad425f8..5230a41405 100644 --- a/pype/api.py +++ b/pype/api.py @@ -15,7 +15,7 @@ from .action import ( RepairContextAction ) -from app.api import Logger +from pypeapp.api import Logger from . import ( Anatomy, diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 3202c19d40..9cca5ea047 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -2,10 +2,10 @@ import toml import time from pype.ftrack import AppAction from avalon import lib -from app.api import Logger +from pypeapp.api import Logger from pype import lib as pypelib -log = Logger.getLogger(__name__) +log = Logger().get_logger(__name__) def registerApp(app, session): diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py index c47c8ac4ac..684b3862a8 100644 --- a/pype/ftrack/actions/action_asset_delete.py +++ b/pype/ftrack/actions/action_asset_delete.py @@ -1,7 +1,7 @@ import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py index 1e2f37ec74..b06a928007 100644 --- a/pype/ftrack/actions/action_client_review_sort.py +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -2,7 +2,7 @@ import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index 579d8ebe85..c40a04b2fd 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -3,7 +3,7 @@ import argparse import logging import subprocess import os -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index fb57221ccd..09749cf2c5 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -2,10 +2,11 @@ import os import sys import argparse import json -import ftrack_api import arrow import logging +from pype.vendor import ftrack_api from pype.ftrack import BaseAction, get_ca_mongoid +from pypeapp.lib.config import get_presets """ This action creates/updates custom attributes. @@ -122,11 +123,6 @@ class CustomAttributes(BaseAction): def __init__(self, session): super().__init__(session) - templates = os.environ['PYPE_STUDIO_TEMPLATES'] - path_items = [ - templates, 'presets', 'ftrack', 'ftrack_custom_attributes.json' - ] - self.filepath = os.path.sep.join(path_items) self.types = {} self.object_type_ids = {} self.groups = {} @@ -230,22 +226,12 @@ class CustomAttributes(BaseAction): self.process_attribute(data) def custom_attributes_from_file(self, session, event): - try: - with open(self.filepath) as data_file: - json_dict = json.load(data_file) - except Exception as e: - msg = ( - 'Loading "Custom attribute file" Failed.' - ' Please check log for more information' - ) - self.log.warning("{} - {}".format(msg, str(e))) - self.show_message(event, msg) - return + presets = get_presets()['ftrack']['ftrack_custom_attributes'] - for cust_attr_name in json_dict: + for cust_attr_name in presets: try: data = {} - cust_attr = json_dict[cust_attr_name] + cust_attr = presets[cust_attr_name] # Get key, label, type data.update(self.get_required(cust_attr)) # Get hierachical/ entity_type/ object_id diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 7ce5526164..cc4023342d 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -4,7 +4,7 @@ import argparse import sys import errno -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction import json from pype import api as pype diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 7a4c15e9fb..838a77570f 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -2,7 +2,7 @@ import sys import logging from bson.objectid import ObjectId import argparse -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction from avalon.tools.libraryloader.io_nonsingleton import DbConnector diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index ee6f875ad3..9da60ce763 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -1,7 +1,7 @@ import sys import logging import argparse -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction from avalon.tools.libraryloader.io_nonsingleton import DbConnector diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py index 018a70b423..377e118ffb 100644 --- a/pype/ftrack/actions/action_delete_unpublished.py +++ b/pype/ftrack/actions/action_delete_unpublished.py @@ -1,7 +1,7 @@ import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index d8e6996db4..4e027b740b 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -5,12 +5,12 @@ import json import logging import subprocess from operator import itemgetter -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseHandler from app.api import Logger from pype import lib -log = Logger.getLogger(__name__) +log = Logger().get_logger(__name__) class DJVViewAction(BaseHandler): diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index d8d0e81cb1..008b36b1c1 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -1,10 +1,8 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2017 ftrack import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py index 3954733041..f6e745b3ec 100644 --- a/pype/ftrack/actions/action_set_version.py +++ b/pype/ftrack/actions/action_set_version.py @@ -1,7 +1,7 @@ import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 68c55be652..d3fb140cc4 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -4,7 +4,7 @@ import argparse import logging import json -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction, lib as ftracklib diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index ad97cba487..36adb99074 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -6,7 +6,7 @@ import os import json import re -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction from avalon import io, inventory, schema diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 2ff6faec48..5b63ec264f 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -3,7 +3,7 @@ import argparse import logging import json -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index 98124aca70..eb5623328e 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -2,7 +2,7 @@ import sys import argparse import logging import json -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/event_collect_entities.py b/pype/ftrack/actions/event_collect_entities.py index d5a34b0153..71f2d26ff3 100644 --- a/pype/ftrack/actions/event_collect_entities.py +++ b/pype/ftrack/actions/event_collect_entities.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/credentials.py b/pype/ftrack/credentials.py index 89353ea984..30d503c534 100644 --- a/pype/ftrack/credentials.py +++ b/pype/ftrack/credentials.py @@ -77,7 +77,6 @@ def _check_credentials(username=None, apiKey=None): session = ftrack_api.Session() session.close() except Exception as e: - print(e) return False return True diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index d795edb08b..e45a0cc8b7 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -2,22 +2,16 @@ import os import json import threading import time -import ftrack_api -from app import style -from app.vendor.Qt import QtCore, QtGui, QtWidgets +from Qt import QtCore, QtGui, QtWidgets -from pype.ftrack import credentials, login_dialog as login_dialog - -from pype.vendor.pynput import mouse, keyboard -from . import FtrackServer +from pype.vendor import ftrack_api +from pypeapp import style +from pype.ftrack import FtrackServer, credentials, login_dialog as login_dialog from pype import api as pype -# load data from templates -pype.load_data_from_templates() - -log = pype.Logger.getLogger(__name__, "ftrack") +log = pype.Logger().get_logger(FtrackModule.__name__, "ftrack") class FtrackRunner: diff --git a/pype/ftrack/ftrack_server/event_server.py b/pype/ftrack/ftrack_server/event_server.py index e824d1d899..2b3acad076 100644 --- a/pype/ftrack/ftrack_server/event_server.py +++ b/pype/ftrack/ftrack_server/event_server.py @@ -1,10 +1,10 @@ import sys from pype.ftrack import credentials, login_dialog as login_dialog from pype.ftrack.ftrack_server import FtrackServer -from app.vendor.Qt import QtWidgets +from Qt import QtWidgets from pype import api -log = api.Logger.getLogger(__name__, "ftrack-event-server") +log = api.Logger().get_logger(__name__, "ftrack-event-server") class EventServer: diff --git a/pype/ftrack/ftrack_server/event_server_cli.py b/pype/ftrack/ftrack_server/event_server_cli.py index a466bf5723..2e9519df26 100644 --- a/pype/ftrack/ftrack_server/event_server_cli.py +++ b/pype/ftrack/ftrack_server/event_server_cli.py @@ -1,9 +1,9 @@ import sys from pype.ftrack import credentials from pype.ftrack.ftrack_server import FtrackServer -from app import api +from pypeapp import api -log = api.Logger.getLogger(__name__, "ftrack-event-server-cli") +log = api.Logger().get_logger(__name__, "ftrack-event-server-cli") possible_yes = ['y', 'yes'] possible_no = ['n', 'no'] diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py index 91caff216e..27207edc48 100644 --- a/pype/ftrack/ftrack_server/ftrack_server.py +++ b/pype/ftrack/ftrack_server/ftrack_server.py @@ -2,12 +2,12 @@ import os import sys import types import importlib -import ftrack_api +from pype.vendor import ftrack_api import time import logging -from app.api import Logger +from pypeapp.api import Logger -log = Logger.getLogger(__name__) +log = Logger().get_logger(__name__) """ # Required - Needed for connection to Ftrack diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index 6c3c9a0be4..56fe5f1ed2 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -8,11 +8,11 @@ import avalon import avalon.api from avalon import schema from avalon.vendor import toml, jsonschema -from app.api import Logger +from pypeapp.api import Logger ValidationError = jsonschema.ValidationError -log = Logger.getLogger(__name__) +log = Logger().get_logger(__name__) def get_ca_mongoid(): diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index a823394bb9..6d56fcb010 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -1,7 +1,7 @@ -import ftrack_api import functools import time from pype import api as pype +from pype.vendor import ftrack_api class MissingPermision(Exception): @@ -27,7 +27,7 @@ class BaseHandler(object): def __init__(self, session): '''Expects a ftrack_api.Session instance''' self._session = session - self.log = pype.Logger.getLogger(self.__class__.__name__) + self.log = pype.Logger().get_logger(self.__class__.__name__) # Using decorator self.register = self.register_decorator(self.register) diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 2828afe539..04ebd59ae4 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -1,7 +1,7 @@ import os import requests -from app.vendor.Qt import QtCore, QtGui, QtWidgets -from app import style +from Qt import QtCore, QtGui, QtWidgets +from pypeapp import style from . import credentials, login_tools diff --git a/pype/ftrack/login_tools.py b/pype/ftrack/login_tools.py index 592ec152ee..b259f2d2ed 100644 --- a/pype/ftrack/login_tools.py +++ b/pype/ftrack/login_tools.py @@ -5,7 +5,7 @@ import webbrowser import functools import pype import inspect -from app.vendor.Qt import QtCore +from Qt import QtCore class LoginServerHandler(BaseHTTPRequestHandler): diff --git a/pype/templates.py b/pype/templates.py index c5578a983c..92dad30e7e 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -3,8 +3,9 @@ import re from avalon import io from avalon import api as avalon from . import lib -from app.api import (Templates, Logger, format) -log = Logger.getLogger(__name__, +# from pypeapp.api import (Templates, Logger, format) +from pypeapp.api import Logger +log = Logger().get_logger(__name__, os.getenv("AVALON_APP", "pype-config")) SESSION = None From 2bd3019a32c51217ca917b1a58ac4578f3494006 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:36:26 +0100 Subject: [PATCH 012/193] ftrack runner renamed to ftrack module --- pype/ftrack/{ftrack_run.py => ftrack_module.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pype/ftrack/{ftrack_run.py => ftrack_module.py} (99%) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_module.py similarity index 99% rename from pype/ftrack/ftrack_run.py rename to pype/ftrack/ftrack_module.py index e45a0cc8b7..cd29f88286 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_module.py @@ -14,7 +14,7 @@ from pype import api as pype log = pype.Logger().get_logger(FtrackModule.__name__, "ftrack") -class FtrackRunner: +class FtrackModule: def __init__(self, main_parent=None, parent=None): self.parent = parent From 2ea5c8b5683e9c3caf5fc3ddc2481ce4f99282e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:36:54 +0100 Subject: [PATCH 013/193] tray init modified --- pype/avalon_apps/__init__.py | 7 ++----- pype/ftrack/__init__.py | 10 +++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pype/avalon_apps/__init__.py b/pype/avalon_apps/__init__.py index 1d7c3c8a81..845f94a330 100644 --- a/pype/avalon_apps/__init__.py +++ b/pype/avalon_apps/__init__.py @@ -1,8 +1,5 @@ from .avalon_app import AvalonApps -def tray_init(tray_widget, main_widget, parent_menu): - av_apps = AvalonApps(main_widget, tray_widget) - av_apps.tray_menu(tray_widget.menu) - - return av_apps +def tray_init(tray_widget, main_widget): + return AvalonApps(main_widget, tray_widget) diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index cdaacca68c..922de28e16 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -1,11 +1,7 @@ from .lib import * from .ftrack_server import * -from .ftrack_run import FtrackRunner +from .ftrack_module import FtrackModule -def tray_init(tray_widget, main_widget, parent_menu): - ftrack = FtrackRunner(main_widget, tray_widget) - main_widget.menu.addMenu(ftrack.trayMenu(parent_menu)) - ftrack.validate() - - return ftrack +def tray_init(tray_widget, main_widget): + return FtrackModule(main_widget, tray_widget) From 67aa4e7d081fab6c092b1e9d0d39d38375e27a0e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Mar 2019 09:44:27 +0100 Subject: [PATCH 014/193] changed log name in ftrack module --- pype/ftrack/ftrack_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/ftrack_module.py b/pype/ftrack/ftrack_module.py index cd29f88286..a68bc2ac26 100644 --- a/pype/ftrack/ftrack_module.py +++ b/pype/ftrack/ftrack_module.py @@ -11,7 +11,7 @@ from pype.ftrack import FtrackServer, credentials, login_dialog as login_dialog from pype import api as pype -log = pype.Logger().get_logger(FtrackModule.__name__, "ftrack") +log = pype.Logger().get_logger("FtrackModule", "ftrack") class FtrackModule: From fc01a4240bef74ead01734b38eca60f77f89f22d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Mar 2019 19:31:19 +0100 Subject: [PATCH 015/193] timers manager tries to load presets before add to idle manager --- .../services/timers_manager/timers_manager.py | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/pype/services/timers_manager/timers_manager.py b/pype/services/timers_manager/timers_manager.py index e6c1c4f18a..5d324ef453 100644 --- a/pype/services/timers_manager/timers_manager.py +++ b/pype/services/timers_manager/timers_manager.py @@ -1,6 +1,7 @@ from Qt import QtCore from .widget_user_idle import WidgetUserIdle from pypeapp.lib.config import get_presets +from pypeapp import Logger class Singleton(type): @@ -20,17 +21,22 @@ class TimersManager(metaclass=Singleton): last_task = None def __init__(self, tray_widget, main_widget): + self.log = Logger().get_logger(self.__class__.__name__) self.tray_widget = tray_widget self.main_widget = main_widget self.widget_user_idle = WidgetUserIdle(self) - self.set_signal_times() def set_signal_times(self): - timer_info = get_presets()['services']['timers_manager']['timer'] - full_time = int(timer_info['full_time'])*60 - message_time = int(timer_info['message_time'])*60 - self.time_show_message = full_time - message_time - self.time_stop_timer = full_time + try: + timer_info = get_presets()['services']['timers_manager']['timer'] + full_time = int(timer_info['full_time'])*60 + message_time = int(timer_info['message_time'])*60 + self.time_show_message = full_time - message_time + self.time_stop_timer = full_time + return True + except Exception: + self.log.warning('Was not able to load presets for TimersManager') + return False def add_module(self, module): self.modules.append(module) @@ -76,28 +82,39 @@ class TimersManager(metaclass=Singleton): self.s_handler = SignalHandler(self) if 'IdleManager' in modules: - self.idle_man = modules['IdleManager'] - # Times when idle is between show widget and stop timers - for num in range(self.time_show_message-1, self.time_stop_timer): - self.idle_man.add_time_signal( - num, - self.s_handler.signal_change_label - ) - # Times when widget is already shown and user restart idle - for num in range(self.time_stop_timer - self.time_show_message): - self.idle_man.add_time_signal( - num, - self.s_handler.signal_change_label - ) - # Time when message is shown + if self.set_signal_times() is True: + self.register_to_idle_manager(modules['IdleManager']) + + def register_to_idle_manager(self, man_obj): + self.idle_man = man_obj + # Times when idle is between show widget and stop timers + show_to_stop_range = range( + self.time_show_message-1, self.time_stop_timer + ) + for num in show_to_stop_range: self.idle_man.add_time_signal( - self.time_show_message, - self.s_handler.signal_show_message + num, + self.s_handler.signal_change_label ) - # Time when timers are stopped + # Times when widget is already shown and user restart idle + shown_and_moved_range = range( + self.time_stop_timer - self.time_show_message + ) + for num in shown_and_moved_range: self.idle_man.add_time_signal( - self.time_stop_timer, self.s_handler.signal_stop_timers + num, + self.s_handler.signal_change_label ) + # Time when message is shown + self.idle_man.add_time_signal( + self.time_show_message, + self.s_handler.signal_show_message + ) + # Time when timers are stopped + self.idle_man.add_time_signal( + self.time_stop_timer, + self.s_handler.signal_stop_timers + ) def change_label(self): if self.is_running is False: From b61cceccee28d1c9ab44615d501e55dd237379a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Mar 2019 19:32:01 +0100 Subject: [PATCH 016/193] added basic docstrings --- pype/avalon_apps/avalon_app.py | 2 +- pype/ftrack/ftrack_module.py | 1 - pype/services/idle_manager/idle_manager.py | 14 +++++++++++ .../services/timers_manager/timers_manager.py | 25 ++++++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index 072d7fbb3f..efad3f3ce0 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -29,7 +29,7 @@ class AvalonApps: icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) - aLibraryLoader = QtWidgets.QAction("&Library", parent_menu) + aLibraryLoader = QtWidgets.QAction("Library", parent_menu) aShowLauncher.triggered.connect(self.show_launcher) aLibraryLoader.triggered.connect(self.show_library_loader) diff --git a/pype/ftrack/ftrack_module.py b/pype/ftrack/ftrack_module.py index a68bc2ac26..127b39d2fc 100644 --- a/pype/ftrack/ftrack_module.py +++ b/pype/ftrack/ftrack_module.py @@ -117,7 +117,6 @@ class FtrackModule: # Menu for Tray App self.menu = QtWidgets.QMenu('Ftrack', parent_menu) self.menu.setProperty('submenu', 'on') - self.menu.setStyleSheet(style.load_stylesheet()) # Actions - server self.smActionS = self.menu.addMenu("Action server") diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py index cc6e177916..e8ba246121 100644 --- a/pype/services/idle_manager/idle_manager.py +++ b/pype/services/idle_manager/idle_manager.py @@ -5,6 +5,10 @@ from pypeapp import Logger class IdleManager(QtCore.QThread): + """ Measure user's idle time in seconds. + Idle time resets on keyboard/mouse input. + Is able to emit signals at specific time idle. + """ time_signals = {} idle_time = 0 signal_reset_timer = QtCore.Signal() @@ -16,6 +20,12 @@ class IdleManager(QtCore.QThread): self._is_running = False def add_time_signal(self, emit_time, signal): + """ If any module want to use IdleManager, need to use add_time_signal + :param emit_time: time when signal will be emitted + :type emit_time: int + :param signal: signal that will be emitted (without objects) + :type signal: QtCore.Signal + """ if emit_time not in self.time_signals: self.time_signals[emit_time] = [] self.time_signals[emit_time].append(signal) @@ -54,6 +64,8 @@ class IdleManager(QtCore.QThread): class MouseThread(QtCore.QThread): + """Listens user's mouse movement + """ signal_stop = QtCore.Signal() def __init__(self, signal): @@ -76,6 +88,8 @@ class MouseThread(QtCore.QThread): class KeyboardThread(QtCore.QThread): + """Listens user's keyboard input + """ signal_stop = QtCore.Signal() def __init__(self, signal): diff --git a/pype/services/timers_manager/timers_manager.py b/pype/services/timers_manager/timers_manager.py index 5d324ef453..6f10a0ec68 100644 --- a/pype/services/timers_manager/timers_manager.py +++ b/pype/services/timers_manager/timers_manager.py @@ -5,6 +5,8 @@ from pypeapp import Logger class Singleton(type): + """ Signleton implementation + """ _instances = {} def __call__(cls, *args, **kwargs): @@ -16,6 +18,12 @@ class Singleton(type): class TimersManager(metaclass=Singleton): + """ Handles about Timers. + + Should be able to start/stop all timers at once. + If IdleManager is imported then is able to handle about stop timers + when user idles for a long time (set in presets). + """ modules = [] is_running = False last_task = None @@ -39,11 +47,20 @@ class TimersManager(metaclass=Singleton): return False def add_module(self, module): + """ Adds module to context + + Module must have implemented methods: + - ``start_timer_manager(data)`` + - ``stop_timer_manager()`` + """ self.modules.append(module) def start_timers(self, data): ''' - Dictionary "data" should contain: + :param data: basic information needed to start any timer + :type data: dict + ..note:: + Dictionary "data" should contain: - project_name(str) - Name of Project - hierarchy(list/tuple) - list of parents(except project) - task_type(str) @@ -53,6 +70,7 @@ class TimersManager(metaclass=Singleton): - to run timers for task in 'C001_BackToPast/assets/characters/villian/Lookdev BG' - input data should contain: + .. code-block:: Python data = { 'project_name': 'C001_BackToPast', 'hierarchy': ['assets', 'characters', 'villian'], @@ -79,6 +97,11 @@ class TimersManager(metaclass=Singleton): self.is_running = False def process_modules(self, modules): + """ Gives ability to connect with imported modules from TrayManager. + + :param modules: All imported modules from TrayManager + :type modules: dict + """ self.s_handler = SignalHandler(self) if 'IdleManager' in modules: From 7e1c8156072609ab1967f063521662f689278ea5 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 8 Apr 2019 22:07:27 +0200 Subject: [PATCH 017/193] change logger imports --- pype/api.py | 2 +- pype/avalon_apps/avalon_app.py | 2 +- .../actions/action_application_loader.py | 2 +- .../actions/action_create_cust_attrs.py | 2 +- pype/ftrack/ftrack_server/event_server_cli.py | 4 +-- pype/ftrack/ftrack_server/ftrack_server.py | 2 +- pype/ftrack/lib/avalon_sync.py | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 31 ++++++++++++++----- pype/plugins/launcher/actions/Aport.py | 2 +- pype/plugins/launcher/actions/AssetCreator.py | 2 +- pype/templates.py | 5 ++- 11 files changed, 35 insertions(+), 21 deletions(-) diff --git a/pype/api.py b/pype/api.py index 5230a41405..aabda8acc9 100644 --- a/pype/api.py +++ b/pype/api.py @@ -15,7 +15,7 @@ from .action import ( RepairContextAction ) -from pypeapp.api import Logger +from pypeapp import Logger from . import ( Anatomy, diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index efad3f3ce0..0b2553c4d9 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -2,7 +2,7 @@ import os import argparse from Qt import QtGui, QtWidgets from avalon.tools import libraryloader -from pypeapp.api import Logger +from pypeapp import Logger from avalon import io from launcher import launcher_widget, lib as launcher_lib diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 9cca5ea047..f98f1b1e0d 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -2,7 +2,7 @@ import toml import time from pype.ftrack import AppAction from avalon import lib -from pypeapp.api import Logger +from pypeapp import Logger from pype import lib as pypelib log = Logger().get_logger(__name__) diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 09749cf2c5..fcd4569f6a 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -6,7 +6,7 @@ import arrow import logging from pype.vendor import ftrack_api from pype.ftrack import BaseAction, get_ca_mongoid -from pypeapp.lib.config import get_presets +from pypeapp.config import get_presets """ This action creates/updates custom attributes. diff --git a/pype/ftrack/ftrack_server/event_server_cli.py b/pype/ftrack/ftrack_server/event_server_cli.py index 2e9519df26..eee2c81eb5 100644 --- a/pype/ftrack/ftrack_server/event_server_cli.py +++ b/pype/ftrack/ftrack_server/event_server_cli.py @@ -1,9 +1,9 @@ import sys from pype.ftrack import credentials from pype.ftrack.ftrack_server import FtrackServer -from pypeapp import api +from pypeapp import Logger -log = api.Logger().get_logger(__name__, "ftrack-event-server-cli") +log = Logger().get_logger(__name__, "ftrack-event-server-cli") possible_yes = ['y', 'yes'] possible_no = ['n', 'no'] diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py index 27207edc48..831d33c4d7 100644 --- a/pype/ftrack/ftrack_server/ftrack_server.py +++ b/pype/ftrack/ftrack_server/ftrack_server.py @@ -5,7 +5,7 @@ import importlib from pype.vendor import ftrack_api import time import logging -from pypeapp.api import Logger +from pypeapp import Logger log = Logger().get_logger(__name__) diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index 56fe5f1ed2..851b6f3ed6 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -8,7 +8,7 @@ import avalon import avalon.api from avalon import schema from avalon.vendor import toml, jsonschema -from pypeapp.api import Logger +from pypeapp import Logger ValidationError = jsonschema.ValidationError diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index fd5b758f22..8f287ad830 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -8,6 +8,8 @@ from pype import lib as pypelib from .avalon_sync import get_config_data from .ftrack_base_handler import BaseHandler +from pypeapp import Anatomy + class AppAction(BaseHandler): '''Custom Action base class @@ -169,7 +171,8 @@ class AppAction(BaseHandler): os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - anatomy = pype.Anatomy + anatomy = Anatomy(project_name=project_name) + hierarchy = "" parents = database[project_name].find_one({ "type": 'asset', @@ -179,20 +182,21 @@ class AppAction(BaseHandler): if parents: hierarchy = os.path.join(*parents) - data = {"project": {"name": entity['project']['full_name'], + data = { + "root": {"work": os.environ.get("PYPE_STUDIO_PROJECTS_PATH")}, + "project": {"name": entity['project']['full_name'], "code": entity['project']['name']}, "task": entity['name'], "asset": entity['parent']['name'], "hierarchy": hierarchy} try: - anatomy = anatomy.format(data) + anatomy_filled = anatomy.format(data) + # anatomy = anatomy.format(data) except Exception as e: self.log.error( "{0} Error in anatomy.format: {1}".format(__name__, e) ) - os.environ["AVALON_WORKDIR"] = os.path.join( - anatomy.work.root, anatomy.work.folder - ) + os.environ["AVALON_WORKDIR"] = anatomy_filled['work']['folder'] # collect all parents from the task parents = [] @@ -210,13 +214,22 @@ class AppAction(BaseHandler): tools_env = acre.get_tools(tools_attr) env = acre.compute(tools_env) env = acre.merge(env, current_env=dict(os.environ)) + env = acre.append(dict(os.environ), env) + + + # + # tools_env = acre.get_tools(tools) + # env = acre.compute(dict(tools_env)) + # env = acre.merge(env, dict(os.environ)) + # os.environ = acre.append(dict(os.environ), env) + # os.environ = acre.compute(os.environ) # Get path to execute - st_temp_path = os.environ['PYPE_STUDIO_TEMPLATES'] + st_temp_path = os.environ['PYPE_CONFIG'] os_plat = platform.system().lower() # Path to folder with launchers - path = os.path.join(st_temp_path, 'bin', os_plat) + path = os.path.join(st_temp_path, 'launchers', os_plat) # Full path to executable launcher execfile = None @@ -321,6 +334,8 @@ class AppAction(BaseHandler): # Set origin avalon environments for key, value in env_origin.items(): + if value == None: + value = "" os.environ[key] = value return { diff --git a/pype/plugins/launcher/actions/Aport.py b/pype/plugins/launcher/actions/Aport.py index 16906f6ce7..3773b90256 100644 --- a/pype/plugins/launcher/actions/Aport.py +++ b/pype/plugins/launcher/actions/Aport.py @@ -9,7 +9,7 @@ import pype.api as pype from pype.api import Logger -log = Logger.getLogger(__name__, "aport") +log = Logger().get_logger(__name__, "aport") class Aport(api.Action): diff --git a/pype/plugins/launcher/actions/AssetCreator.py b/pype/plugins/launcher/actions/AssetCreator.py index d6875bd7ff..579edebcea 100644 --- a/pype/plugins/launcher/actions/AssetCreator.py +++ b/pype/plugins/launcher/actions/AssetCreator.py @@ -7,7 +7,7 @@ from pype.tools import assetcreator from pype.api import Logger -log = Logger.getLogger(__name__, "aport") +log = Logger().get_logger(__name__, "asset_creator") class AssetCreator(api.Action): diff --git a/pype/templates.py b/pype/templates.py index 92dad30e7e..58ae54f466 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -4,9 +4,8 @@ from avalon import io from avalon import api as avalon from . import lib # from pypeapp.api import (Templates, Logger, format) -from pypeapp.api import Logger -log = Logger().get_logger(__name__, - os.getenv("AVALON_APP", "pype-config")) +from pypeapp import Logger +log = Logger().get_logger(__name__, os.getenv("AVALON_APP", "pype-config")) SESSION = None From 100aafa6ec34b9a1b1fffd03db5f8dfecbad03ba Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 8 Apr 2019 23:00:24 +0200 Subject: [PATCH 018/193] change PYPE_STUDIO_TEMPLATES to PYPE_CONFIG --- pype/lib.py | 2 +- pype/plugins/aport/publish/collect_context.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 43461582db..48f14fe6c4 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -467,7 +467,7 @@ def get_all_avalon_projects(): def get_presets_path(): - templates = os.environ['PYPE_STUDIO_TEMPLATES'] + templates = os.environ['PYPE_CONFIG'] path_items = [templates, 'presets'] filepath = os.path.sep.join(path_items) return filepath diff --git a/pype/plugins/aport/publish/collect_context.py b/pype/plugins/aport/publish/collect_context.py index 4e27cefd09..f43e78120c 100644 --- a/pype/plugins/aport/publish/collect_context.py +++ b/pype/plugins/aport/publish/collect_context.py @@ -63,8 +63,8 @@ class CollectContextDataFromAport(pyblish.api.ContextPlugin): pyblish.api.register_host(host) # get path to studio templates - templates_dir = os.getenv("PYPE_STUDIO_TEMPLATES", None) - assert templates_dir, "Missing `PYPE_STUDIO_TEMPLATES` in os.environ" + templates_dir = os.getenv("PYPE_CONFIG", None) + assert templates_dir, "Missing `PYPE_CONFIG` in os.environ" # get presets for host presets_dir = os.path.join(templates_dir, "presets", host) From 2bb789c413ccdb5c75f0eb027d51fb8c3edb3b2d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:22:55 +0200 Subject: [PATCH 019/193] logger fixes --- pype/aport/__init__.py | 2 +- pype/aport/api.py | 2 +- pype/aport/pipeline.py | 2 +- pype/aport/templates.py | 2 +- pype/lib.py | 12 +++++++----- pype/nuke/templates.py | 2 +- pype/plugins/nuke/create/create_read.py | 2 +- pype/plugins/nuke/create/create_write.py | 3 ++- pype/plugins/nuke/load/actions.py | 2 +- pype/plugins/nuke/load/load_sequence.py | 2 +- pype/plugins/nuke/publish/collect_reads.py | 2 +- pype/plugins/nuke/publish/collect_writes.py | 2 +- pype/premiere/__init__.py | 2 +- pype/premiere/templates.py | 2 +- setup/nuke/nuke_path/menu.py | 8 ++++---- 15 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pype/aport/__init__.py b/pype/aport/__init__.py index 4efcb731c7..acab2d3770 100644 --- a/pype/aport/__init__.py +++ b/pype/aport/__init__.py @@ -7,7 +7,7 @@ from app import api as app from .. import api -log = api.Logger.getLogger(__name__, "aport") +log = api.Logger.get_logger(__name__, "aport") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") diff --git a/pype/aport/api.py b/pype/aport/api.py index 4d202b6e7a..bbd7c814c3 100644 --- a/pype/aport/api.py +++ b/pype/aport/api.py @@ -19,7 +19,7 @@ from app.api import forward from pype import api as pype -log = pype.Logger.getLogger(__name__, "aport") +log = pype.Logger.get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/aport/pipeline.py b/pype/aport/pipeline.py index dee41aff34..458a60241f 100644 --- a/pype/aport/pipeline.py +++ b/pype/aport/pipeline.py @@ -20,7 +20,7 @@ for name, handler in [(handler.get_name(), handler) if "pype" not in str(name).lower(): pype.Logger.logging.root.removeHandler(handler) -log = pype.Logger.getLogger(__name__, "aport") +log = pype.Logger.get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/aport/templates.py b/pype/aport/templates.py index 5be6e276ba..b9d8e708ea 100644 --- a/pype/aport/templates.py +++ b/pype/aport/templates.py @@ -1,6 +1,6 @@ from pype import api as pype -log = pype.Logger.getLogger(__name__, "aport") +log = pype.Logger.get_logger(__name__, "aport") def get_anatomy(**kwarg): diff --git a/pype/lib.py b/pype/lib.py index 48f14fe6c4..ece5a01e22 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -420,7 +420,9 @@ def get_avalon_project_template_schema(): def get_avalon_project_template(): - from app.api import Templates + # from app.api import Templates + + from pypeapp import Anatomy """ Get avalon template @@ -428,11 +430,11 @@ def get_avalon_project_template(): Returns: dictionary with templates """ - template = Templates(type=["anatomy"]) + anatomy = Anatomy(project_name=os.environ.get('AVALON_PROJECT')).anatomy proj_template = {} - proj_template['workfile'] = template.anatomy.avalon.workfile - proj_template['work'] = template.anatomy.avalon.work - proj_template['publish'] = template.anatomy.avalon.publish + proj_template['workfile'] = anatomy["avalon"]["workfile"] + proj_template['work'] = anatomy["avalon"]["work"] + proj_template['publish'] = anatomy["avalon"]["publish"] return proj_template diff --git a/pype/nuke/templates.py b/pype/nuke/templates.py index 16cb6062a2..4be350cbcb 100644 --- a/pype/nuke/templates.py +++ b/pype/nuke/templates.py @@ -1,6 +1,6 @@ from pype import api as pype -log = pype.Logger.getLogger(__name__, "nuke") +log = pype.Logger().get_logger(__name__, "nuke") def get_anatomy(**kwarg): diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 8597737167..8ab902cfc0 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -6,7 +6,7 @@ from pype import api as pype import nuke -log = pype.Logger.getLogger(__name__, "nuke") +log = pype.Logger.get_logger(__name__, "nuke") class CrateRead(avalon.nuke.Creator): diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py index af7462680e..83e6b043ed 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write.py @@ -5,11 +5,12 @@ from pype.nuke import ( create_write_node ) from pype import api as pype +# from pypeapp import Logger import nuke -log = pype.Logger.getLogger(__name__, "nuke") +log = pype.Logger().get_logger(__name__, "nuke") def subset_to_families(subset, family, families): diff --git a/pype/plugins/nuke/load/actions.py b/pype/plugins/nuke/load/actions.py index 449567987a..917e7e71b0 100644 --- a/pype/plugins/nuke/load/actions.py +++ b/pype/plugins/nuke/load/actions.py @@ -5,7 +5,7 @@ from avalon import api from pype.api import Logger -log = Logger.getLogger(__name__, "nuke") +log = Logger().get_logger(__name__, "nuke") class SetFrameRangeLoader(api.Loader): diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index a4a591e657..c82d697541 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -8,7 +8,7 @@ import avalon.io as io import nuke from pype.api import Logger -log = Logger.getLogger(__name__, "nuke") +log = Logger.get_looger(__name__, "nuke") @contextlib.contextmanager diff --git a/pype/plugins/nuke/publish/collect_reads.py b/pype/plugins/nuke/publish/collect_reads.py index f5d3008b40..75ea2efa3a 100644 --- a/pype/plugins/nuke/publish/collect_reads.py +++ b/pype/plugins/nuke/publish/collect_reads.py @@ -6,7 +6,7 @@ import pyblish.api import logging from avalon import io, api -log = logging.getLogger(__name__) +log = logging.get_logger(__name__) @pyblish.api.log diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 2a274201bb..612c2a8775 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -6,7 +6,7 @@ import logging import pype.api as pype -log = logging.getLogger(__name__) +log = logging.get_logger(__name__) @pyblish.api.log diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 3d6610795e..2fe583bb55 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -12,7 +12,7 @@ from .. import api import requests -log = api.Logger.getLogger(__name__, "premiere") +log = api.Logger.get_logger(__name__, "premiere") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") EXTENSIONS_PATH_LOCAL = os.getenv("EXTENSIONS_PATH", None) diff --git a/pype/premiere/templates.py b/pype/premiere/templates.py index 33a7a6ff61..1a426c0d5d 100644 --- a/pype/premiere/templates.py +++ b/pype/premiere/templates.py @@ -1,6 +1,6 @@ from pype import api as pype -log = pype.Logger.getLogger(__name__, "premiere") +log = pype.Logger.get_logger(__name__, "premiere") def get_anatomy(**kwarg): diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 45f44d0d11..9a96a52850 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -1,12 +1,12 @@ from pype.nuke.lib import writes_version_sync, onScriptLoad import nuke -from pype.api import Logger +from pypeapp import Logger -log = Logger.getLogger(__name__, "nuke") +log = Logger().get_logger(__name__, "nuke") -# nuke.addOnScriptSave(writes_version_sync) -# nuke.addOnScriptSave(onScriptLoad) +nuke.addOnScriptSave(writes_version_sync) +nuke.addOnScriptSave(onScriptLoad) log.info('Automatic syncing of write file knob to script version') From 53523d1f4f725aa03c9e4e9812dbe698276f7a07 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:26:30 +0200 Subject: [PATCH 020/193] more logging --- pype/aport/original/api.py | 2 +- pype/nuke/__init__.py | 94 +++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/pype/aport/original/api.py b/pype/aport/original/api.py index bc2a71a08c..dcc7f804f7 100644 --- a/pype/aport/original/api.py +++ b/pype/aport/original/api.py @@ -19,7 +19,7 @@ from app.api import forward from pype import api as pype -log = pype.Logger.getLogger(__name__, "aport") +log = pype.Logger.get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index ae00342f09..db04cda58f 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -13,14 +13,18 @@ from .lib import ( import nuke -# removing logger handler created in avalon_core -for name, handler in [(handler.get_name(), handler) - for handler in api.Logger.logging.root.handlers[:]]: - if "pype" not in str(name).lower(): - api.Logger.logging.root.removeHandler(handler) +from pypeapp import Logger + +# #removing logger handler created in avalon_core +# for name, handler in [(handler.get_name(), handler) +# for handler in Logger.logging.root.handlers[:]]: +# if "pype" not in str(name).lower(): +# Logger.logging.root.removeHandler(handler) -log = api.Logger.getLogger(__name__, "nuke") +log = Logger().get_logger(__name__, "nuke") + +# log = api.Logger.getLogger(__name__, "nuke") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") @@ -37,40 +41,40 @@ self = sys.modules[__name__] self.nLogger = None -class NukeHandler(api.Logger.logging.Handler): - ''' - Nuke Handler - emits logs into nuke's script editor. - warning will emit nuke.warning() - critical and fatal would popup msg dialog to alert of the error. - ''' +# class NukeHandler(Logger.logging.Handler): +# ''' +# Nuke Handler - emits logs into nuke's script editor. +# warning will emit nuke.warning() +# critical and fatal would popup msg dialog to alert of the error. +# ''' +# +# def __init__(self): +# api.Logger.logging.Handler.__init__(self) +# self.set_name("Pype_Nuke_Handler") +# +# def emit(self, record): +# # Formated message: +# msg = self.format(record) +# +# if record.levelname.lower() in [ +# # "warning", +# "critical", +# "fatal", +# "error" +# ]: +# nuke.message(msg) - def __init__(self): - api.Logger.logging.Handler.__init__(self) - self.set_name("Pype_Nuke_Handler") - - def emit(self, record): - # Formated message: - msg = self.format(record) - - if record.levelname.lower() in [ - # "warning", - "critical", - "fatal", - "error" - ]: - nuke.message(msg) - - -'''Adding Nuke Logging Handler''' -nuke_handler = NukeHandler() -if nuke_handler.get_name() \ - not in [handler.get_name() - for handler in api.Logger.logging.root.handlers[:]]: - api.Logger.logging.getLogger().addHandler(nuke_handler) - api.Logger.logging.getLogger().setLevel(api.Logger.logging.INFO) - -if not self.nLogger: - self.nLogger = api.Logger +# +# '''Adding Nuke Logging Handler''' +# nuke_handler = NukeHandler() +# if nuke_handler.get_name() \ +# not in [handler.get_name() +# for handler in Logger.logging.root.handlers[:]]: +# api.Logger.logging.getLogger().addHandler(nuke_handler) +# api.Logger.logging.getLogger().setLevel(Logger.logging.INFO) +# +# if not self.nLogger: +# self.nLogger = Logger def reload_config(): @@ -101,8 +105,14 @@ def reload_config(): def install(): - api.set_avalon_workdir() - reload_config() + # api.set_avalon_workdir() + # reload_config() + + import sys + + for path in sys.path: + if path.startswith("C:\\Users\\Public"): + sys.path.remove(path) log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) @@ -124,7 +134,7 @@ def install(): menu.install() # load data from templates - api.load_data_from_templates() + # api.load_data_from_templates() def uninstall(): From 7e5e20dbcc0adc089700270e97d893a38f2854c5 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:27:26 +0200 Subject: [PATCH 021/193] move icons --- pype/plugins/global/publish/collect_templates.py | 10 +++++++--- pype/plugins/global/publish/integrate.py | 8 ++++---- .../global/publish/integrate_rendered_frames.py | 6 +++--- {res => pype/res}/ftrack/sign_in_message.html | 0 {res => pype/res}/icons/Thumbs.db | Bin {res => pype/res}/icons/inventory.png | Bin {res => pype/res}/icons/loader.png | Bin {res => pype/res}/icons/lookmanager.png | Bin {res => pype/res}/icons/workfiles.png | Bin {res => pype/res}/workspace.mel | 0 10 files changed, 14 insertions(+), 10 deletions(-) rename {res => pype/res}/ftrack/sign_in_message.html (100%) rename {res => pype/res}/icons/Thumbs.db (100%) rename {res => pype/res}/icons/inventory.png (100%) rename {res => pype/res}/icons/loader.png (100%) rename {res => pype/res}/icons/lookmanager.png (100%) rename {res => pype/res}/icons/workfiles.png (100%) rename {res => pype/res}/workspace.mel (100%) diff --git a/pype/plugins/global/publish/collect_templates.py b/pype/plugins/global/publish/collect_templates.py index fb6de894bd..3fa603a2ae 100644 --- a/pype/plugins/global/publish/collect_templates.py +++ b/pype/plugins/global/publish/collect_templates.py @@ -1,7 +1,9 @@ -import pype.api as pype +# import pype.api as pype import pyblish.api +from pypeapp import Anatomy +import os class CollectTemplates(pyblish.api.ContextPlugin): @@ -11,6 +13,8 @@ class CollectTemplates(pyblish.api.ContextPlugin): label = "Collect Templates" def process(self, context): - pype.load_data_from_templates() - context.data['anatomy'] = pype.Anatomy + # pype.load_data_from_templates() + + anatomy = Anatomy(project_name=os.environ.get("AVALON_PROJECT")) + context.data['anatomy'] = anatomy self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index be7fc3bcf3..530c00f9ed 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -200,10 +200,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.publish.path + dst = anatomy_filled["publish"]["path"] instance.data["transfers"].append([src, dst]) - template = anatomy.publish.path + template = anatomy.anatomy["publish"]["path"] else: # Single file @@ -224,10 +224,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.publish.path + dst = anatomy_filled["publish"]["path"] instance.data["transfers"].append([src, dst]) - template = anatomy.publish.path + template = anatomy.anatomy["publish"]["path"] representation = { "schema": "pype:representation-2.0", diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index ae11d33348..626f03093a 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -192,7 +192,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = src_collection.format( "{padding}") % i anatomy_filled = anatomy.format(template_data) - test_dest_files.append(anatomy_filled.render.path) + test_dest_files.append(anatomy_filled["render"]["path"]) dst_collections, remainder = clique.assemble(test_dest_files) dst_collection = dst_collections[0] @@ -242,8 +242,8 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = "#####" anatomy_filled = anatomy.format(template_data) - path_to_save = anatomy_filled.render.path - template = anatomy.render.fullpath + path_to_save = anatomy_filled["render"]["path"] + template = anatomy["render"]["path"] self.log.debug('ext[1:]: {}'.format(ext[1:])) representation = { diff --git a/res/ftrack/sign_in_message.html b/pype/res/ftrack/sign_in_message.html similarity index 100% rename from res/ftrack/sign_in_message.html rename to pype/res/ftrack/sign_in_message.html diff --git a/res/icons/Thumbs.db b/pype/res/icons/Thumbs.db similarity index 100% rename from res/icons/Thumbs.db rename to pype/res/icons/Thumbs.db diff --git a/res/icons/inventory.png b/pype/res/icons/inventory.png similarity index 100% rename from res/icons/inventory.png rename to pype/res/icons/inventory.png diff --git a/res/icons/loader.png b/pype/res/icons/loader.png similarity index 100% rename from res/icons/loader.png rename to pype/res/icons/loader.png diff --git a/res/icons/lookmanager.png b/pype/res/icons/lookmanager.png similarity index 100% rename from res/icons/lookmanager.png rename to pype/res/icons/lookmanager.png diff --git a/res/icons/workfiles.png b/pype/res/icons/workfiles.png similarity index 100% rename from res/icons/workfiles.png rename to pype/res/icons/workfiles.png diff --git a/res/workspace.mel b/pype/res/workspace.mel similarity index 100% rename from res/workspace.mel rename to pype/res/workspace.mel From d98d92e410517cfdc383afeec78684c0b1b0b569 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:27:44 +0200 Subject: [PATCH 022/193] Revert "move icons" This reverts commit 7e5e20dbcc0adc089700270e97d893a38f2854c5. --- pype/plugins/global/publish/collect_templates.py | 10 +++------- pype/plugins/global/publish/integrate.py | 8 ++++---- .../global/publish/integrate_rendered_frames.py | 6 +++--- {pype/res => res}/ftrack/sign_in_message.html | 0 {pype/res => res}/icons/Thumbs.db | Bin {pype/res => res}/icons/inventory.png | Bin {pype/res => res}/icons/loader.png | Bin {pype/res => res}/icons/lookmanager.png | Bin {pype/res => res}/icons/workfiles.png | Bin {pype/res => res}/workspace.mel | 0 10 files changed, 10 insertions(+), 14 deletions(-) rename {pype/res => res}/ftrack/sign_in_message.html (100%) rename {pype/res => res}/icons/Thumbs.db (100%) rename {pype/res => res}/icons/inventory.png (100%) rename {pype/res => res}/icons/loader.png (100%) rename {pype/res => res}/icons/lookmanager.png (100%) rename {pype/res => res}/icons/workfiles.png (100%) rename {pype/res => res}/workspace.mel (100%) diff --git a/pype/plugins/global/publish/collect_templates.py b/pype/plugins/global/publish/collect_templates.py index 3fa603a2ae..fb6de894bd 100644 --- a/pype/plugins/global/publish/collect_templates.py +++ b/pype/plugins/global/publish/collect_templates.py @@ -1,9 +1,7 @@ -# import pype.api as pype +import pype.api as pype import pyblish.api -from pypeapp import Anatomy -import os class CollectTemplates(pyblish.api.ContextPlugin): @@ -13,8 +11,6 @@ class CollectTemplates(pyblish.api.ContextPlugin): label = "Collect Templates" def process(self, context): - # pype.load_data_from_templates() - - anatomy = Anatomy(project_name=os.environ.get("AVALON_PROJECT")) - context.data['anatomy'] = anatomy + pype.load_data_from_templates() + context.data['anatomy'] = pype.Anatomy self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 530c00f9ed..be7fc3bcf3 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -200,10 +200,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled["publish"]["path"] + dst = anatomy_filled.publish.path instance.data["transfers"].append([src, dst]) - template = anatomy.anatomy["publish"]["path"] + template = anatomy.publish.path else: # Single file @@ -224,10 +224,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled["publish"]["path"] + dst = anatomy_filled.publish.path instance.data["transfers"].append([src, dst]) - template = anatomy.anatomy["publish"]["path"] + template = anatomy.publish.path representation = { "schema": "pype:representation-2.0", diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 626f03093a..ae11d33348 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -192,7 +192,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = src_collection.format( "{padding}") % i anatomy_filled = anatomy.format(template_data) - test_dest_files.append(anatomy_filled["render"]["path"]) + test_dest_files.append(anatomy_filled.render.path) dst_collections, remainder = clique.assemble(test_dest_files) dst_collection = dst_collections[0] @@ -242,8 +242,8 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = "#####" anatomy_filled = anatomy.format(template_data) - path_to_save = anatomy_filled["render"]["path"] - template = anatomy["render"]["path"] + path_to_save = anatomy_filled.render.path + template = anatomy.render.fullpath self.log.debug('ext[1:]: {}'.format(ext[1:])) representation = { diff --git a/pype/res/ftrack/sign_in_message.html b/res/ftrack/sign_in_message.html similarity index 100% rename from pype/res/ftrack/sign_in_message.html rename to res/ftrack/sign_in_message.html diff --git a/pype/res/icons/Thumbs.db b/res/icons/Thumbs.db similarity index 100% rename from pype/res/icons/Thumbs.db rename to res/icons/Thumbs.db diff --git a/pype/res/icons/inventory.png b/res/icons/inventory.png similarity index 100% rename from pype/res/icons/inventory.png rename to res/icons/inventory.png diff --git a/pype/res/icons/loader.png b/res/icons/loader.png similarity index 100% rename from pype/res/icons/loader.png rename to res/icons/loader.png diff --git a/pype/res/icons/lookmanager.png b/res/icons/lookmanager.png similarity index 100% rename from pype/res/icons/lookmanager.png rename to res/icons/lookmanager.png diff --git a/pype/res/icons/workfiles.png b/res/icons/workfiles.png similarity index 100% rename from pype/res/icons/workfiles.png rename to res/icons/workfiles.png diff --git a/pype/res/workspace.mel b/res/workspace.mel similarity index 100% rename from pype/res/workspace.mel rename to res/workspace.mel From ba6d14c7e151a654127a990fb585e004c7454ac5 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:28:15 +0200 Subject: [PATCH 023/193] temporarily turn of templates in nuke --- pype/__init__.py | 10 +++++----- pype/api.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pype/__init__.py b/pype/__init__.py index 8bd31c060d..755ffa9e7a 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -9,11 +9,11 @@ from .lib import collect_container_metadata import logging log = logging.getLogger(__name__) -# do not delete these are mandatory -Anatomy = None -Dataflow = None -Metadata = None -Colorspace = None +# # do not delete these are mandatory +# Anatomy = None +# Dataflow = None +# Metadata = None +# Colorspace = None PACKAGE_DIR = os.path.dirname(__file__) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") diff --git a/pype/api.py b/pype/api.py index aabda8acc9..e42c58fab7 100644 --- a/pype/api.py +++ b/pype/api.py @@ -17,12 +17,12 @@ from .action import ( from pypeapp import Logger -from . import ( - Anatomy, - Colorspace, - Metadata, - Dataflow -) +# from . import ( +# Anatomy, +# Colorspace, +# Metadata, +# Dataflow +# ) from .templates import ( load_data_from_templates, From 28dbe25f16fc65fd0e6d9a7043eede347327aea2 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:28:27 +0200 Subject: [PATCH 024/193] use new anatomy --- pype/ftrack/lib/ftrack_app_handler.py | 4 ++-- pype/nuke/lib.py | 4 +++- pype/plugins/global/publish/collect_assumed_destination.py | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 8f287ad830..6d07a39f81 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -183,7 +183,7 @@ class AppAction(BaseHandler): hierarchy = os.path.join(*parents) data = { - "root": {"work": os.environ.get("PYPE_STUDIO_PROJECTS_PATH")}, + "root": os.environ.get("PYPE_STUDIO_PROJECTS_PATH"), "project": {"name": entity['project']['full_name'], "code": entity['project']['name']}, "task": entity['name'], @@ -217,7 +217,7 @@ class AppAction(BaseHandler): env = acre.append(dict(os.environ), env) - # + # # tools_env = acre.get_tools(tools) # env = acre.compute(dict(tools_env)) # env = acre.merge(env, dict(os.environ)) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 0f29484d9f..adc5f6b14a 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -7,7 +7,9 @@ import avalon.nuke import pype.api as pype import nuke -log = pype.Logger.getLogger(__name__, "nuke") +from pypeapp import Logger +log = Logger().get_logger(__name__, "nuke") + self = sys.modules[__name__] self._project = None diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index d5d3d9a846..058af12340 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -19,12 +19,15 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): self.create_destination_template(instance) template_data = instance.data["assumedTemplateData"] - # template = instance.data["template"] + template = instance.data["template"] anatomy = instance.context.data['anatomy'] + # self.log.info(anatomy.anatomy()) + self.log.info(anatomy.anatomy) # template = anatomy.publish.path anatomy_filled = anatomy.format(template_data) - mock_template = anatomy_filled.publish.path + self.log.info(anatomy_filled) + mock_template = anatomy_filled["publish"]["path"] # For now assume resources end up in a "resources" folder in the # published folder From 0e39b2fee1f17b08682fb3c3226b98244948e65a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 18:54:37 +0200 Subject: [PATCH 025/193] evenmore logging --- pype/aport/__init__.py | 2 +- pype/aport/api.py | 2 +- pype/aport/original/api.py | 2 +- pype/aport/pipeline.py | 2 +- pype/aport/templates.py | 2 +- pype/plugins/nuke/create/create_read.py | 2 +- pype/premiere/__init__.py | 2 +- pype/premiere/templates.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/aport/__init__.py b/pype/aport/__init__.py index acab2d3770..68d16ace9f 100644 --- a/pype/aport/__init__.py +++ b/pype/aport/__init__.py @@ -7,7 +7,7 @@ from app import api as app from .. import api -log = api.Logger.get_logger(__name__, "aport") +log = api.Logger().get_logger(__name__, "aport") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") diff --git a/pype/aport/api.py b/pype/aport/api.py index bbd7c814c3..1b84536285 100644 --- a/pype/aport/api.py +++ b/pype/aport/api.py @@ -19,7 +19,7 @@ from app.api import forward from pype import api as pype -log = pype.Logger.get_logger(__name__, "aport") +log = pype.Logger().get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/aport/original/api.py b/pype/aport/original/api.py index dcc7f804f7..8ed24402b4 100644 --- a/pype/aport/original/api.py +++ b/pype/aport/original/api.py @@ -19,7 +19,7 @@ from app.api import forward from pype import api as pype -log = pype.Logger.get_logger(__name__, "aport") +log = pype.Logger().get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/aport/pipeline.py b/pype/aport/pipeline.py index 458a60241f..4c9cf7aa60 100644 --- a/pype/aport/pipeline.py +++ b/pype/aport/pipeline.py @@ -20,7 +20,7 @@ for name, handler in [(handler.get_name(), handler) if "pype" not in str(name).lower(): pype.Logger.logging.root.removeHandler(handler) -log = pype.Logger.get_logger(__name__, "aport") +log = pype.Logger().get_logger(__name__, "aport") SESSION = avalon.session diff --git a/pype/aport/templates.py b/pype/aport/templates.py index b9d8e708ea..2db1d58004 100644 --- a/pype/aport/templates.py +++ b/pype/aport/templates.py @@ -1,6 +1,6 @@ from pype import api as pype -log = pype.Logger.get_logger(__name__, "aport") +log = pype.Logger().get_logger(__name__, "aport") def get_anatomy(**kwarg): diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 8ab902cfc0..e033bc63b0 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -6,7 +6,7 @@ from pype import api as pype import nuke -log = pype.Logger.get_logger(__name__, "nuke") +log = pype.Logger().get_logger(__name__, "nuke") class CrateRead(avalon.nuke.Creator): diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 2fe583bb55..3217d61010 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -12,7 +12,7 @@ from .. import api import requests -log = api.Logger.get_logger(__name__, "premiere") +log = api.Logger().get_logger(__name__, "premiere") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") EXTENSIONS_PATH_LOCAL = os.getenv("EXTENSIONS_PATH", None) diff --git a/pype/premiere/templates.py b/pype/premiere/templates.py index 1a426c0d5d..e53d529cc1 100644 --- a/pype/premiere/templates.py +++ b/pype/premiere/templates.py @@ -1,6 +1,6 @@ from pype import api as pype -log = pype.Logger.get_logger(__name__, "premiere") +log = pype.Logger().get_logger(__name__, "premiere") def get_anatomy(**kwarg): From 7fe8d02abf8c1ef00a528a5a253e607b06cdc452 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 9 Apr 2019 23:00:22 +0200 Subject: [PATCH 026/193] Merge branch 'develop' into 2.0/sync_from_1.0 # Conflicts: # pype/ftrack/actions/action_djvview.py # pype/ftrack/actions/event_collect_entities.py # pype/ftrack/lib/ftrack_app_handler.py # pype/plugins/global/publish/collect_assumed_destination.py # pype/plugins/launcher/actions/AssetCreator.py --- pype/ftrack/lib/ftrack_base_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 3e08eb122d..faba858792 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -1,7 +1,7 @@ import functools import time from pype import api as pype -from pype.vendor import ftrack_api +import ftrack_api class MissingPermision(Exception): From 698c59c8ea0e76caed606fadd2b100a61b60b9d7 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Apr 2019 00:12:09 +0200 Subject: [PATCH 027/193] convert presets to pype2.0 way --- .../actions/action_create_cust_attrs.py | 4 +- pype/ftrack/lib/ftrack_app_handler.py | 2 + pype/plugins/maya/load/load_alembic.py | 12 ++---- pype/plugins/maya/load/load_ass.py | 20 +++------- pype/plugins/maya/load/load_camera.py | 11 ++---- pype/plugins/maya/load/load_fbx.py | 12 ++---- pype/plugins/maya/load/load_mayaascii.py | 12 ++---- pype/plugins/maya/load/load_model.py | 38 +++++-------------- pype/plugins/maya/load/load_rig.py | 11 ++---- .../plugins/maya/load/load_vdb_to_redshift.py | 13 ++----- pype/plugins/maya/load/load_vdb_to_vray.py | 12 ++---- pype/plugins/maya/load/load_vrayproxy.py | 13 ++----- pype/plugins/maya/load/load_yeti_cache.py | 11 ++---- pype/plugins/maya/load/load_yeti_rig.py | 12 ++---- 14 files changed, 55 insertions(+), 128 deletions(-) diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index fcd4569f6a..9f2564406a 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -6,7 +6,7 @@ import arrow import logging from pype.vendor import ftrack_api from pype.ftrack import BaseAction, get_ca_mongoid -from pypeapp.config import get_presets +from pypeapp import config """ This action creates/updates custom attributes. @@ -226,7 +226,7 @@ class CustomAttributes(BaseAction): self.process_attribute(data) def custom_attributes_from_file(self, session, event): - presets = get_presets()['ftrack']['ftrack_custom_attributes'] + presets = config.get_presets()['ftrack']['ftrack_custom_attributes'] for cust_attr_name in presets: try: diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 7498db5b62..138b6d6459 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -201,6 +201,8 @@ class AppAction(BaseHandler): if parents: hierarchy = os.path.join(*parents) + application = avalonlib.get_application(os.environ["AVALON_APP_NAME"]) + data = { "root": os.environ.get("PYPE_STUDIO_PROJECTS_PATH"), "project": {"name": entity['project']['full_name'], diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 9fd4aa2108..d3d85249c5 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -1,6 +1,6 @@ import pype.maya.plugin import os -import json +from pypeapp import config class AbcLoader(pype.maya.plugin.ReferenceLoader): @@ -36,14 +36,8 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) - + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index c268ce70c5..979d4b5767 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -2,7 +2,7 @@ from avalon import api import pype.maya.plugin import os import pymel.core as pm -import json +from pypeapp import config class AssProxyLoader(pype.maya.plugin.ReferenceLoader): @@ -50,13 +50,8 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): proxyShape.dso.set(path) proxyShape.aiOverrideShaders.set(0) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: @@ -165,13 +160,8 @@ class AssStandinLoader(api.Loader): label = "{}:{}".format(namespace, name) root = pm.group(name=label, empty=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get('ass') if c is not None: diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index 989e80e979..e9bf265b98 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -1,6 +1,6 @@ import pype.maya.plugin import os -import json +from pypeapp import config class CameraLoader(pype.maya.plugin.ReferenceLoader): @@ -35,13 +35,8 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): cameras = cmds.ls(nodes, type="camera") - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_fbx.py b/pype/plugins/maya/load/load_fbx.py index b580257334..14df300c3c 100644 --- a/pype/plugins/maya/load/load_fbx.py +++ b/pype/plugins/maya/load/load_fbx.py @@ -1,6 +1,6 @@ import pype.maya.plugin import os -import json +from pypeapp import config class FBXLoader(pype.maya.plugin.ReferenceLoader): @@ -36,13 +36,9 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): groupName="{}:{}".format(namespace, name)) groupName = "{}:{}".format(namespace, name) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 549d1dff4c..03a15b0524 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -1,5 +1,5 @@ import pype.maya.plugin -import json +from pypeapp import config import os @@ -36,13 +36,9 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): self[:] = nodes groupName = "{}:{}".format(namespace, name) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 16f3556de7..913d4d9e79 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -1,7 +1,8 @@ from avalon import api import pype.maya.plugin -import json import os +from pypeapp import config +reload(config) class ModelLoader(pype.maya.plugin.ReferenceLoader): @@ -21,18 +22,6 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya - try: - family = context["representation"]["context"]["family"] - except ValueError: - family = "model" - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) - with maya.maintained_selection(): groupName = "{}:{}".format(namespace, name) @@ -46,7 +35,9 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - c = colors.get(family) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] + c = colors.get('model') if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", @@ -89,14 +80,9 @@ class GpuCacheLoader(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get('model') if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) @@ -196,14 +182,8 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) - + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get('model') if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index 1dcff45bb9..66b086c861 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -3,7 +3,7 @@ from maya import cmds import pype.maya.plugin from avalon import api, maya import os -import json +from pypeapp import config class RigLoader(pype.maya.plugin.ReferenceLoader): @@ -39,13 +39,8 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py index 169c3bf34a..ee7c301b1b 100644 --- a/pype/plugins/maya/load/load_vdb_to_redshift.py +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -1,7 +1,6 @@ from avalon import api import os -import json - +from pypeapp import config class LoadVDBtoRedShift(api.Loader): """Load OpenVDB in a Redshift Volume Shape""" @@ -55,13 +54,9 @@ class LoadVDBtoRedShift(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py index 58d6d1b56e..3b15b71e3e 100644 --- a/pype/plugins/maya/load/load_vdb_to_vray.py +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -1,5 +1,5 @@ from avalon import api -import json +from pypeapp import config import os @@ -47,13 +47,9 @@ class LoadVDBtoVRay(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_vrayproxy.py b/pype/plugins/maya/load/load_vrayproxy.py index a3a114440a..9b07dc7e30 100644 --- a/pype/plugins/maya/load/load_vrayproxy.py +++ b/pype/plugins/maya/load/load_vrayproxy.py @@ -1,6 +1,6 @@ from avalon.maya import lib from avalon import api -import json +from pypeapp import config import os import maya.cmds as cmds @@ -26,14 +26,6 @@ class VRayProxyLoader(api.Loader): except ValueError: family = "vrayproxy" - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) - asset_name = context['asset']["name"] namespace = namespace or lib.unique_namespace( asset_name + "_", @@ -54,6 +46,9 @@ class VRayProxyLoader(api.Loader): if not nodes: return + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] + c = colors.get(family) if c is not None: cmds.setAttr("{0}_{1}.useOutlinerColor".format(name, "GRP"), 1) diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index b19bed1393..dc976c0c98 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -9,6 +9,7 @@ from maya import cmds from avalon import api from avalon.maya import lib as avalon_lib, pipeline from pype.maya import lib +from pypeapp import config class YetiCacheLoader(api.Loader): @@ -54,13 +55,9 @@ class YetiCacheLoader(api.Loader): group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/pype/plugins/maya/load/load_yeti_rig.py b/pype/plugins/maya/load/load_yeti_rig.py index c821c6ca02..eb75ff6bdc 100644 --- a/pype/plugins/maya/load/load_yeti_rig.py +++ b/pype/plugins/maya/load/load_yeti_rig.py @@ -1,6 +1,6 @@ import pype.maya.plugin import os -import json +from pypeapp import config class YetiRigLoader(pype.maya.plugin.ReferenceLoader): @@ -27,13 +27,9 @@ class YetiRigLoader(pype.maya.plugin.ReferenceLoader): groupName="{}:{}".format(namespace, name)) groupName = "{}:{}".format(namespace, name) - preset_file = os.path.join( - os.environ.get('PYPE_STUDIO_TEMPLATES'), - 'presets', 'tools', - 'family_colors.json' - ) - with open(preset_file, 'r') as cfile: - colors = json.load(cfile) + + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] c = colors.get('yetiRig') if c is not None: From e08b026e9dbe60586f39778a68743b63d16e25a2 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 12 Apr 2019 18:26:39 +0200 Subject: [PATCH 028/193] fix anatomy --- pype/ftrack/lib/ftrack_app_handler.py | 2 +- pype/lib.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 6d07a39f81..c80453ea47 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -171,7 +171,7 @@ class AppAction(BaseHandler): os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - anatomy = Anatomy(project_name=project_name) + anatomy = Anatomy() hierarchy = "" parents = database[project_name].find_one({ diff --git a/pype/lib.py b/pype/lib.py index ece5a01e22..78773f4f23 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -430,11 +430,11 @@ def get_avalon_project_template(): Returns: dictionary with templates """ - anatomy = Anatomy(project_name=os.environ.get('AVALON_PROJECT')).anatomy + templates = Anatomy().templates proj_template = {} - proj_template['workfile'] = anatomy["avalon"]["workfile"] - proj_template['work'] = anatomy["avalon"]["work"] - proj_template['publish'] = anatomy["avalon"]["publish"] + proj_template['workfile'] = templates["avalon"]["workfile"] + proj_template['work'] = templates["avalon"]["work"] + proj_template['publish'] = templates["avalon"]["publish"] return proj_template From b262c304ce37a60606e3ebd53da943fbafb68535 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 12 Apr 2019 18:53:14 +0200 Subject: [PATCH 029/193] fix logging --- pype/ftrack/lib/ftrack_app_handler.py | 2 +- pype/plugins/launcher/actions/ClockifyStart.py | 2 +- pype/plugins/launcher/actions/ClockifySync.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 138b6d6459..f5c7cfc68b 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -190,7 +190,7 @@ class AppAction(BaseHandler): os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - anatomy = Anatomy(project_name=project_name) + anatomy = Anatomy(project=project_name) hierarchy = "" parents = database[project_name].find_one({ diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index 78a8b4e1b6..9183805c7f 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -5,7 +5,7 @@ try: except Exception: pass -log = Logger.getLogger(__name__, "clockify_start") +log = Logger().get_logger(__name__, "clockify_start") class ClockifyStart(api.Action): diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index c50fbc4b25..0895da555d 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -4,7 +4,7 @@ try: except Exception: pass from pype.api import Logger -log = Logger.getLogger(__name__, "clockify_sync") +log = Logger().get_logger(__name__, "clockify_sync") class ClockifySync(api.Action): From 44b9e48a677d59810f96dfb205676fba4ed72a12 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 12 Apr 2019 23:14:45 +0200 Subject: [PATCH 030/193] hotfix/convert old app code to pypeapp --- pype/aport/__init__.py | 6 +++--- pype/aport/api.py | 4 ++-- pype/aport/original/api.py | 8 ++++---- pype/aport/pipeline.py | 4 ++-- pype/clockify/clockify.py | 4 ++-- pype/clockify/widget_settings.py | 4 ++-- pype/ftrack/actions/action_djvview.py | 2 +- pype/ftrack/actions/action_rv.py | 4 ++-- pype/ftrack/events/event_test.py | 1 - pype/lib.py | 2 -- pype/plugins/global/publish/validate_templates.py | 9 +++------ pype/premiere/__init__.py | 7 ++----- pype/widgets/project_settings.py | 5 +---- 13 files changed, 24 insertions(+), 36 deletions(-) diff --git a/pype/aport/__init__.py b/pype/aport/__init__.py index 68d16ace9f..9e1bde0a15 100644 --- a/pype/aport/__init__.py +++ b/pype/aport/__init__.py @@ -3,11 +3,11 @@ import sys from avalon import api as avalon from pyblish import api as pyblish -from app import api as app +from pypeapp import execute, Logger from .. import api -log = api.Logger().get_logger(__name__, "aport") +log = Logger().get_logger(__name__, "aport") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") @@ -83,7 +83,7 @@ def pico_server_launch(): "api" ] - app.forward( + execute( args, cwd=path ) diff --git a/pype/aport/api.py b/pype/aport/api.py index 1b84536285..42a090dc63 100644 --- a/pype/aport/api.py +++ b/pype/aport/api.py @@ -15,7 +15,7 @@ from avalon import io import pyblish.api as pyblish -from app.api import forward +from pypeapp import execute from pype import api as pype @@ -70,7 +70,7 @@ def publish(json_data_path, gui): log.debug(args) # start standalone pyblish qml - forward([ + execute([ sys.executable, "-u" ] + args, cwd=cwd diff --git a/pype/aport/original/api.py b/pype/aport/original/api.py index 8ed24402b4..b7ae447546 100644 --- a/pype/aport/original/api.py +++ b/pype/aport/original/api.py @@ -15,7 +15,7 @@ from avalon import io import pyblish.api as pyblish -from app.api import forward +from pypeapp import execute from pype import api as pype @@ -68,7 +68,7 @@ def publish(json_data_path, staging_dir=None): log.debug(args) # start standalone pyblish qml - forward([ + execute([ sys.executable, "-u" ] + args, cwd=cwd @@ -239,8 +239,8 @@ app.register_module(__name__) # remove all Handlers created by pico for name, handler in [(handler.get_name(), handler) - for handler in pype.Logger.logging.root.handlers[:]]: + for handler in Logger().logging.root.handlers[:]]: if "pype" not in str(name).lower(): print(name) print(handler) - pype.Logger.logging.root.removeHandler(handler) + Logger().logging.root.removeHandler(handler) diff --git a/pype/aport/pipeline.py b/pype/aport/pipeline.py index 4c9cf7aa60..2c37695225 100644 --- a/pype/aport/pipeline.py +++ b/pype/aport/pipeline.py @@ -11,7 +11,7 @@ from avalon import io import pyblish.api as pyblish -from app.api import forward +from pypeapp import execute from pype import api as pype # remove all Handlers created by pico @@ -67,7 +67,7 @@ def publish(json_data_path, staging_dir=None): log.debug(args) # start standalone pyblish qml - forward([ + execute([ sys.executable, "-u" ] + args, cwd=os.getenv('AVALON_WORKDIR').replace("\\", "/") diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index a22933f700..17be642be5 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,6 +1,6 @@ import threading -from app import style -from app.vendor.Qt import QtWidgets +from pypeapp import style +from Qt import QtWidgets from pype.clockify import ClockifySettings, ClockifyAPI diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index 02fd4350e6..ad92c299bb 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -1,6 +1,6 @@ import os -from app.vendor.Qt import QtCore, QtGui, QtWidgets -from app import style +from Qt import QtCore, QtGui, QtWidgets +from pypeapp import style class ClockifySettings(QtWidgets.QWidget): diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 7da83457ee..3a4298fa02 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -7,7 +7,7 @@ import subprocess from operator import itemgetter import ftrack_api from pype.ftrack import BaseAction -from app.api import Logger +from pypeapp import Logger from pype import lib as pypelib diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index d5ff83e8c8..89a426ab9d 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -8,9 +8,9 @@ import logging import operator import re from pype import lib as pypelib -from app.api import Logger +from pypeapp import Logger -log = Logger.getLogger(__name__) +log = Logger().get_logger(__name__) class RVAction(BaseAction): diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py index 46e16cbb95..e4da4cd44e 100644 --- a/pype/ftrack/events/event_test.py +++ b/pype/ftrack/events/event_test.py @@ -3,7 +3,6 @@ import sys import re import ftrack_api from pype.ftrack import BaseEvent -from app import api ignore_me = True diff --git a/pype/lib.py b/pype/lib.py index 78773f4f23..176ef45967 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -420,8 +420,6 @@ def get_avalon_project_template_schema(): def get_avalon_project_template(): - # from app.api import Templates - from pypeapp import Anatomy """ diff --git a/pype/plugins/global/publish/validate_templates.py b/pype/plugins/global/publish/validate_templates.py index 8f8eb45686..5611a6f1f0 100644 --- a/pype/plugins/global/publish/validate_templates.py +++ b/pype/plugins/global/publish/validate_templates.py @@ -1,7 +1,4 @@ import pyblish.api -from app.api import ( - Templates -) class ValidateTemplates(pyblish.api.ContextPlugin): """Check if all templates were filed""" @@ -14,7 +11,7 @@ class ValidateTemplates(pyblish.api.ContextPlugin): anatomy = context.data["anatomy"] if not anatomy: - raise RuntimeError("Did not find templates") + raise RuntimeError("Did not find anatomy") else: data = { "project": {"name": "D001_projectsx", "code": "prjX"}, @@ -26,7 +23,7 @@ class ValidateTemplates(pyblish.api.ContextPlugin): anatomy = context.data["anatomy"].format(data) - self.log.info(anatomy.work.path) + self.log.info(anatomy["work"]["path"]) data = { "project": {"name": "D001_projectsy", "code": "prjY"}, @@ -37,4 +34,4 @@ class ValidateTemplates(pyblish.api.ContextPlugin): "hierarchy": "ep101/sq01/bob"} anatomy = context.data["anatomy"].format(data) - self.log.info(anatomy.work.file) + self.log.info(anatomy["work"]["file"]) diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 3217d61010..74ce106de2 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -1,18 +1,15 @@ import os -import sys from pysync import walktree from avalon import api as avalon from pyblish import api as pyblish -from app import api as app -from pprint import pprint +from pypeapp import Logger from .. import api import requests - -log = api.Logger().get_logger(__name__, "premiere") +log = Logger().get_logger(__name__, "premiere") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") EXTENSIONS_PATH_LOCAL = os.getenv("EXTENSIONS_PATH", None) diff --git a/pype/widgets/project_settings.py b/pype/widgets/project_settings.py index 98c97b4885..3aa2fc06b6 100644 --- a/pype/widgets/project_settings.py +++ b/pype/widgets/project_settings.py @@ -1,6 +1,6 @@ -from app import style +from pypeapp import style from avalon.vendor.Qt import QtCore, QtGui, QtWidgets import os import getpass @@ -9,9 +9,6 @@ import platform import ftrack_api -# object symbol - - class Project_name_getUI(QtWidgets.QWidget): ''' Project setting ui: here all the neceserry ui widgets are created From dce33a93702b17098618ba56987f79b0019d0826 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 13 Apr 2019 01:06:48 +0200 Subject: [PATCH 031/193] convert anatomy in plugins to new --- .../actions/action_application_loader.py | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 6 +++--- pype/ftrack/lib/ftrack_base_handler.py | 4 ++-- .../publish/collect_assumed_destination.py | 4 ++-- .../global/publish/collect_templates.py | 5 +++-- pype/plugins/global/publish/integrate.py | 8 ++++---- .../global/publish/validate_templates.py | 20 +++++++++++-------- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index ecf3575902..50714e4535 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -63,4 +63,4 @@ def register(session): time.sleep(0.1) app_counter += 1 except Exception as e: - log.warning("'{0}' - not proper App ({1})".format(app['name'], e)) + log.exception("'{0}' - not proper App ({1})".format(app['name'], e)) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index ca5875b322..e4075e9a19 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -227,10 +227,10 @@ class AppAction(BaseHandler): except Exception: try: anatomy = anatomy.format(data) - work_template = anatomy["work"]["path"] + work_template = anatomy["work"]["folder"] except Exception as e: - self.log.error( + self.log.exception( "{0} Error in anatomy.format: {1}".format(__name__, e) ) os.environ["AVALON_WORKDIR"] = os.path.normpath(work_template) @@ -296,7 +296,7 @@ class AppAction(BaseHandler): try: fp = open(execfile) except PermissionError as p: - self.log.error('Access denied on {0} - {1}'.format( + self.log.exception('Access denied on {0} - {1}'.format( execfile, p)) return { 'success': False, diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index faba858792..7a04ba329c 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -81,7 +81,7 @@ class BaseHandler(object): self.type, label) ) except Exception as e: - self.log.error('{} "{}" - Registration failed ({})'.format( + self.log.exception('{} "{}" - Registration failed ({})'.format( self.type, label, str(e)) ) return wrapper_register @@ -104,7 +104,7 @@ class BaseHandler(object): return result except Exception as e: msg = '{} "{}": Failed ({})'.format(self.type, label, str(e)) - self.log.error(msg) + self.log.exception(msg) return { 'success': False, 'message': msg diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 16a299d524..7458db6aa7 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -23,7 +23,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): anatomy = instance.context.data['anatomy'] # self.log.info(anatomy.anatomy()) - self.log.info(anatomy.anatomy) + self.log.info(anatomy.templates) # template = anatomy.publish.path anatomy_filled = anatomy.format(template_data) self.log.info(anatomy_filled) @@ -137,5 +137,5 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): # We take the parent folder of representation 'filepath' instance.data["assumedDestination"] = os.path.dirname( - (anatomy.format(template_data)).publish.path + (anatomy.format(template_data))["publish"]["path"] ) diff --git a/pype/plugins/global/publish/collect_templates.py b/pype/plugins/global/publish/collect_templates.py index fb6de894bd..b59b20892b 100644 --- a/pype/plugins/global/publish/collect_templates.py +++ b/pype/plugins/global/publish/collect_templates.py @@ -1,5 +1,6 @@ import pype.api as pype +from pypeapp import Anatomy import pyblish.api @@ -11,6 +12,6 @@ class CollectTemplates(pyblish.api.ContextPlugin): label = "Collect Templates" def process(self, context): - pype.load_data_from_templates() - context.data['anatomy'] = pype.Anatomy + # pype.load_data_from_templates() + context.data['anatomy'] = Anatomy() self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 00096a95ee..fce9d26220 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -210,10 +210,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.publish.path + dst = anatomy_filled["publish"]["path"] instance.data["transfers"].append([src, dst]) - template = anatomy.publish.path + template = anatomy.templates["publish"]["path"] else: # Single file @@ -234,10 +234,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.publish.path + dst = anatomy_filled["publish"]["path"] instance.data["transfers"].append([src, dst]) - template = anatomy.publish.path + template = anatomy.templates["publish"]["path"] representation = { "schema": "pype:representation-2.0", diff --git a/pype/plugins/global/publish/validate_templates.py b/pype/plugins/global/publish/validate_templates.py index 5611a6f1f0..a2c7b24ac1 100644 --- a/pype/plugins/global/publish/validate_templates.py +++ b/pype/plugins/global/publish/validate_templates.py @@ -1,4 +1,5 @@ import pyblish.api +import os class ValidateTemplates(pyblish.api.ContextPlugin): """Check if all templates were filed""" @@ -13,25 +14,28 @@ class ValidateTemplates(pyblish.api.ContextPlugin): if not anatomy: raise RuntimeError("Did not find anatomy") else: - data = { "project": {"name": "D001_projectsx", + data = { + "root": os.environ["PYPE_STUDIO_PROJECTS_PATH"], + "project": {"name": "D001_projectsx", "code": "prjX"}, - "representation": "exr", + "ext": "exr", "version": 3, "task": "animation", "asset": "sh001", "hierarchy": "ep101/sq01/sh010"} - anatomy = context.data["anatomy"].format(data) - self.log.info(anatomy["work"]["path"]) + anatomy_filled = anatomy.format(data) + self.log.info(anatomy_filled) - data = { "project": {"name": "D001_projectsy", + data = {"root": os.environ["PYPE_STUDIO_PROJECTS_PATH"], + "project": {"name": "D001_projectsy", "code": "prjY"}, - "representation": "abc", + "ext": "abc", "version": 1, "task": "lookdev", "asset": "bob", "hierarchy": "ep101/sq01/bob"} - anatomy = context.data["anatomy"].format(data) - self.log.info(anatomy["work"]["file"]) + anatomy_filled = context.data["anatomy"].format(data) + self.log.info(anatomy_filled["work"]["folder"]) From bb39f87f81ae3b7a0dcbaf0d1c3d85dcf58e5f13 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 13 Apr 2019 18:13:34 +0200 Subject: [PATCH 032/193] changin ftrack_api imports to run from pype.vendor for now --- pype/ftrack/actions/action_clockify_start.py | 2 +- pype/ftrack/actions/action_clockify_sync.py | 2 +- pype/ftrack/actions/action_create_folders.py | 33 +++++++------ .../actions/action_create_project_folders.py | 49 +++++++++---------- pype/ftrack/actions/action_djvview.py | 13 +++-- pype/ftrack/actions/action_multiple_notes.py | 4 +- pype/ftrack/actions/action_rv.py | 10 ++-- 7 files changed, 55 insertions(+), 58 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index b1c60a2525..594ec21b78 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -2,7 +2,7 @@ import sys import argparse import logging -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction from pype.clockify import ClockifyAPI diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 202bb7b912..4cc00225e2 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -2,7 +2,7 @@ import sys import argparse import logging import json -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction, MissingPermision from pype.clockify import ClockifyAPI diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index c871201701..4426fb9650 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -3,13 +3,14 @@ import sys import logging import argparse import re -import json +# import json from pype.vendor import ftrack_api from pype.ftrack import BaseAction -from pype import api as pype, lib as pypelib +# from pype import api as pype, lib as pypelib from avalon import lib as avalonlib from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from pypeapp import config, Anatomy class CreateFolders(BaseAction): @@ -130,12 +131,12 @@ class CreateFolders(BaseAction): template_publish = av_project['config']['template']['publish'] self.db.uninstall() except Exception: - anatomy = pype.Anatomy - template_work = anatomy.avalon.work - template_publish = anatomy.avalon.publish + templates = Anatomy().templates + template_work = templates["avalon"]["work"] + template_publish = templates["avalon"]["publish"] collected_paths = [] - presets = self.get_presets() + presets = config.get_presets()['tools']['sw_folders'] for entity in all_entities: if entity.entity_type.lower() == 'project': continue @@ -238,16 +239,16 @@ class CreateFolders(BaseAction): output.extend(self.get_notask_children(child)) return output - def get_presets(self): - fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] - filepath = os.path.normpath(os.path.sep.join(fpath_items)) - presets = dict() - try: - with open(filepath) as data_file: - presets = json.load(data_file) - except Exception as e: - self.log.warning('Wasn\'t able to load presets') - return dict(presets) + # def get_presets(self): + # fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] + # filepath = os.path.normpath(os.path.sep.join(fpath_items)) + # presets = dict() + # try: + # with open(filepath) as data_file: + # presets = json.load(data_file) + # except Exception as e: + # self.log.warning('Wasn\'t able to load presets') + # return dict(presets) def template_format(self, template, data): diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index 15bd18cb5f..66e2e153e6 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -3,11 +3,10 @@ import sys import re import argparse import logging -import json -import ftrack_api -from pype import lib as pypelib +from pype.vendor import ftrack_api from pype.ftrack import BaseAction +from pypeapp import config class CreateProjectFolders(BaseAction): @@ -43,7 +42,7 @@ class CreateProjectFolders(BaseAction): else: project = entity['project'] - presets = self.load_presets() + presets = config.load_presets()['tools']['project_folder_structure'] try: # Get paths based on presets basic_paths = self.get_path_items(presets) @@ -143,27 +142,27 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - def load_presets(self): - preset_items = [ - pypelib.get_presets_path(), - 'tools', - 'project_folder_structure.json' - ] - filepath = os.path.sep.join(preset_items) - - # Load folder structure template from presets - presets = dict() - try: - with open(filepath) as data_file: - presets = json.load(data_file) - except Exception as e: - msg = 'Unable to load Folder structure preset' - self.log.warning(msg) - return { - 'success': False, - 'message': msg - } - return presets + # def load_presets(self): + # preset_items = [ + # pypelib.get_presets_path(), + # 'tools', + # 'project_folder_structure.json' + # ] + # filepath = os.path.sep.join(preset_items) + # + # # Load folder structure template from presets + # presets = dict() + # try: + # with open(filepath) as data_file: + # presets = json.load(data_file) + # except Exception as e: + # msg = 'Unable to load Folder structure preset' + # self.log.warning(msg) + # return { + # 'success': False, + # 'message': msg + # } + # return presets def get_path_items(self, in_dict): output = [] diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 3a4298fa02..942aa7a327 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -1,15 +1,12 @@ import os import sys -import re import json import logging import subprocess from operator import itemgetter -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction -from pypeapp import Logger -from pype import lib as pypelib - +from pypeapp import Logger, config log = Logger().get_logger(__name__) @@ -28,7 +25,8 @@ class DJVViewAction(BaseAction): self.djv_path = None self.config_data = None - self.load_config_data() + # self.load_config_data() + self.config_data = config.get_presets()['djv_view']['config'] self.set_djv_path() if self.djv_path is None: @@ -57,7 +55,8 @@ class DJVViewAction(BaseAction): return False def load_config_data(self): - path_items = [pypelib.get_presets_path(), 'djv_view', 'config.json'] + # path_items = [pypelib.get_presets_path(), 'djv_view', 'config.json'] + path_items = config.get_presets()['djv_view']['config'] filepath = os.path.sep.join(path_items) data = dict() diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index c61f5b1e9c..2d93f64242 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -1,9 +1,7 @@ -import os import sys import argparse import logging -import json -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 89a426ab9d..15689ae811 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -3,12 +3,11 @@ import os import sys import json import subprocess -import ftrack_api +from pype.vendor import ftrack_api import logging import operator import re -from pype import lib as pypelib -from pypeapp import Logger +from pypeapp import Logger, config log = Logger().get_logger(__name__) @@ -40,7 +39,8 @@ class RVAction(BaseAction): ) else: # if not, fallback to config file location - self.load_config_data() + # self.load_config_data() + self.config_data = config.get_presets()['djv_view']['config'] self.set_rv_path() if self.rv_path is None: @@ -62,7 +62,7 @@ class RVAction(BaseAction): return False def load_config_data(self): - path_items = [pypelib.get_presets_path(), 'rv', 'config.json'] + path_items = config.get_presets['rv']['config.json'] filepath = os.path.sep.join(path_items) data = dict() From 67f314c276e25b029ed871a39de2c36d4cf76a4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:17:50 +0200 Subject: [PATCH 033/193] initial commit --- pype/tools/standalonepublish/__init__.py | 8 ++++++++ pype/tools/standalonepublish/__main__.py | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 pype/tools/standalonepublish/__init__.py create mode 100644 pype/tools/standalonepublish/__main__.py diff --git a/pype/tools/standalonepublish/__init__.py b/pype/tools/standalonepublish/__init__.py new file mode 100644 index 0000000000..29a4e52904 --- /dev/null +++ b/pype/tools/standalonepublish/__init__.py @@ -0,0 +1,8 @@ +from .app import ( + show, + cli +) +__all__ = [ + "show", + "cli" +] diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/pype/tools/standalonepublish/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) From e6eb2a25e70548f7fafa3229d305ccc2310a2309 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:18:14 +0200 Subject: [PATCH 034/193] added resources for widget icons --- .../standalonepublish/resources/__init__.py | 14 +++++ .../standalonepublish/resources/edit.svg | 9 +++ .../resources/image_sequence.png | Bin 0 -> 5092 bytes .../resources/information.svg | 14 +++++ .../resources/original/delete-button.svg | 55 ++++++++++++++++++ .../resources/original/information.svg | 44 ++++++++++++++ .../resources/original/picture.svg | 48 +++++++++++++++ .../resources/original/play_icon.svg | 19 ++++++ .../standalonepublish/resources/preview.svg | 19 ++++++ .../standalonepublish/resources/thumbnail.svg | 19 ++++++ .../standalonepublish/resources/trash.svg | 23 ++++++++ 11 files changed, 264 insertions(+) create mode 100644 pype/tools/standalonepublish/resources/__init__.py create mode 100644 pype/tools/standalonepublish/resources/edit.svg create mode 100644 pype/tools/standalonepublish/resources/image_sequence.png create mode 100644 pype/tools/standalonepublish/resources/information.svg create mode 100644 pype/tools/standalonepublish/resources/original/delete-button.svg create mode 100644 pype/tools/standalonepublish/resources/original/information.svg create mode 100644 pype/tools/standalonepublish/resources/original/picture.svg create mode 100644 pype/tools/standalonepublish/resources/original/play_icon.svg create mode 100644 pype/tools/standalonepublish/resources/preview.svg create mode 100644 pype/tools/standalonepublish/resources/thumbnail.svg create mode 100644 pype/tools/standalonepublish/resources/trash.svg diff --git a/pype/tools/standalonepublish/resources/__init__.py b/pype/tools/standalonepublish/resources/__init__.py new file mode 100644 index 0000000000..ce329ee585 --- /dev/null +++ b/pype/tools/standalonepublish/resources/__init__.py @@ -0,0 +1,14 @@ +import os + + +resource_path = os.path.dirname(__file__) + + +def get_resource(*args): + """ Serves to simple resources access + + :param \*args: should contain *subfolder* names and *filename* of + resource from resources folder + :type \*args: list + """ + return os.path.normpath(os.path.join(resource_path, *args)) diff --git a/pype/tools/standalonepublish/resources/edit.svg b/pype/tools/standalonepublish/resources/edit.svg new file mode 100644 index 0000000000..26451b4a9d --- /dev/null +++ b/pype/tools/standalonepublish/resources/edit.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/image_sequence.png b/pype/tools/standalonepublish/resources/image_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..51639b07d0f94612a64a8521fff0509660debe9b GIT binary patch literal 5092 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 literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/information.svg b/pype/tools/standalonepublish/resources/information.svg new file mode 100644 index 0000000000..e0f73a7eb1 --- /dev/null +++ b/pype/tools/standalonepublish/resources/information.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/delete-button.svg b/pype/tools/standalonepublish/resources/original/delete-button.svg new file mode 100644 index 0000000000..48b09ac787 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/delete-button.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/information.svg b/pype/tools/standalonepublish/resources/original/information.svg new file mode 100644 index 0000000000..c040bab773 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/information.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/picture.svg b/pype/tools/standalonepublish/resources/original/picture.svg new file mode 100644 index 0000000000..35f912ce80 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/picture.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/play_icon.svg b/pype/tools/standalonepublish/resources/original/play_icon.svg new file mode 100644 index 0000000000..e9bab5a251 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/play_icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/preview.svg b/pype/tools/standalonepublish/resources/preview.svg new file mode 100644 index 0000000000..4a9810c1d5 --- /dev/null +++ b/pype/tools/standalonepublish/resources/preview.svg @@ -0,0 +1,19 @@ + + + + + PREVIEW + + diff --git a/pype/tools/standalonepublish/resources/thumbnail.svg b/pype/tools/standalonepublish/resources/thumbnail.svg new file mode 100644 index 0000000000..dbc228f8c8 --- /dev/null +++ b/pype/tools/standalonepublish/resources/thumbnail.svg @@ -0,0 +1,19 @@ + + + + + THUMBNAIL + + diff --git a/pype/tools/standalonepublish/resources/trash.svg b/pype/tools/standalonepublish/resources/trash.svg new file mode 100644 index 0000000000..07905024c0 --- /dev/null +++ b/pype/tools/standalonepublish/resources/trash.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + From 0d73f32d26338a330ca05d5248a5589b4ddf5d5b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:20:52 +0200 Subject: [PATCH 035/193] created svg button --- .../standalonepublish/widgets/__init__.py | 11 ++ .../widgets/button_from_svgs.py | 109 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/__init__.py create mode 100644 pype/tools/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py new file mode 100644 index 0000000000..f6b3dc2fff --- /dev/null +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -0,0 +1,11 @@ +from avalon.vendor.Qt import * +from avalon.vendor import qtawesome as awesome +from avalon import style + +HelpRole = QtCore.Qt.UserRole + 2 +FamilyRole = QtCore.Qt.UserRole + 3 +ExistsRole = QtCore.Qt.UserRole + 4 +PluginRole = QtCore.Qt.UserRole + 5 + +from ..resources import get_resource +from .button_from_svgs import SvgResizable, SvgButton diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py new file mode 100644 index 0000000000..aeeeae5c7a --- /dev/null +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -0,0 +1,109 @@ +from xml.dom import minidom + +from . import QtGui, QtCore, QtWidgets +from PyQt5 import QtSvg, QtXml + + +class SvgResizable(QtSvg.QSvgWidget): + clicked = QtCore.Signal() + def __init__(self, filepath, width=None, height=None, fill=None): + super().__init__() + self.xmldoc = minidom.parse(filepath) + itemlist = self.xmldoc.getElementsByTagName('svg') + for element in itemlist: + if fill: + element.setAttribute('fill', str(fill)) + if width is not None and height is not None: + self.setMaximumSize(width, height) + self.setMinimumSize(width, height) + xml_string = self.xmldoc.toxml() + svg_bytes = bytearray(xml_string, encoding='utf-8') + + self.load(svg_bytes) + + def change_color(self, color): + element = self.xmldoc.getElementsByTagName('svg')[0] + element.setAttribute('fill', str(color)) + xml_string = self.xmldoc.toxml() + svg_bytes = bytearray(xml_string, encoding='utf-8') + self.load(svg_bytes) + + def mousePressEvent(self, event): + self.clicked.emit() + + +class SvgButton(QtWidgets.QFrame): + clicked = QtCore.Signal() + def __init__( + self, filepath, width=None, height=None, fills=[], + parent=None, checkable=True + ): + super().__init__(parent) + self.checkable = checkable + self.checked = False + + xmldoc = minidom.parse(filepath) + element = xmldoc.getElementsByTagName('svg')[0] + c_actual = '#777777' + if element.hasAttribute('fill'): + c_actual = element.getAttribute('fill') + self.store_fills(fills, c_actual) + + self.installEventFilter(self) + self.svg_widget = SvgResizable(filepath, width, height, self.c_normal) + xmldoc = minidom.parse(filepath) + + layout = QtWidgets.QHBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.svg_widget) + + if width is not None and height is not None: + self.setMaximumSize(width, height) + self.setMinimumSize(width, height) + + def store_fills(self, fills, actual): + if len(fills) == 0: + fills = [actual, actual, actual, actual] + elif len(fills) == 1: + fills = [fills[0], fills[0], fills[0], fills[0]] + elif len(fills) == 2: + fills = [fills[0], fills[1], fills[1], fills[1]] + elif len(fills) == 3: + fills = [fills[0], fills[1], fills[2], fills[2]] + self.c_normal = fills[0] + self.c_hover = fills[1] + self.c_active = fills[2] + self.c_active_hover = fills[3] + + def eventFilter(self, object, event): + if event.type() == QtCore.QEvent.Enter: + self.hoverEnterEvent(event) + return True + elif event.type() == QtCore.QEvent.Leave: + self.hoverLeaveEvent(event) + return True + elif event.type() == QtCore.QEvent.MouseButtonRelease: + self.mousePressEvent(event) + return False + + def change_checked(self, in_bool=False): + if self.checkable: + self.checked = in_bool + + def hoverEnterEvent(self, event=None): + color = self.c_hover + if self.checked: + color = self.c_active_hover + self.svg_widget.change_color(color) + + def hoverLeaveEvent(self, event=None): + color = self.c_normal + if self.checked: + color = self.c_active + self.svg_widget.change_color(color) + + def mousePressEvent(self, event=None): + self.change_checked(not self.checked) + self.hoverEnterEvent() + self.clicked.emit() From d4619716403fc3ebbd69397e64e3051bb6f33d90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:24:09 +0200 Subject: [PATCH 036/193] added widgets for asset selection --- .../standalonepublish/widgets/__init__.py | 10 + .../standalonepublish/widgets/model_asset.py | 158 +++++++++++ .../widgets/model_filter_proxy_exact_match.py | 28 ++ .../model_filter_proxy_recursive_sort.py | 30 +++ .../standalonepublish/widgets/model_node.py | 56 ++++ .../standalonepublish/widgets/model_tree.py | 122 +++++++++ .../widgets/model_tree_view_deselectable.py | 16 ++ .../standalonepublish/widgets/widget_asset.py | 255 ++++++++++++++++++ .../widgets/widget_asset_view.py | 16 ++ 9 files changed, 691 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/model_asset.py create mode 100644 pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py create mode 100644 pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py create mode 100644 pype/tools/standalonepublish/widgets/model_node.py create mode 100644 pype/tools/standalonepublish/widgets/model_tree.py create mode 100644 pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py create mode 100644 pype/tools/standalonepublish/widgets/widget_asset.py create mode 100644 pype/tools/standalonepublish/widgets/widget_asset_view.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index f6b3dc2fff..426fa3d33c 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -9,3 +9,13 @@ PluginRole = QtCore.Qt.UserRole + 5 from ..resources import get_resource from .button_from_svgs import SvgResizable, SvgButton + +from .model_node import Node +from .model_tree import TreeModel +from .model_asset import AssetModel +from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel +from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel +from .model_tree_view_deselectable import DeselectableTreeView + +from .widget_asset_view import AssetView +from .widget_asset import AssetWidget diff --git a/pype/tools/standalonepublish/widgets/model_asset.py b/pype/tools/standalonepublish/widgets/model_asset.py new file mode 100644 index 0000000000..fdf844342e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_asset.py @@ -0,0 +1,158 @@ +import logging +from . import QtCore, QtGui +from . import TreeModel, Node +from . import style, awesome + + +log = logging.getLogger(__name__) + + +def _iter_model_rows(model, + column, + include_root=False): + """Iterate over all row indices in a model""" + indices = [QtCore.QModelIndex()] # start iteration at root + + for index in indices: + + # Add children to the iterations + child_rows = model.rowCount(index) + for child_row in range(child_rows): + child_index = model.index(child_row, column, index) + indices.append(child_index) + + if not include_root and not index.isValid(): + continue + + yield index + + +class AssetModel(TreeModel): + """A model listing assets in the silo in the active project. + + The assets are displayed in a treeview, they are visually parented by + a `visualParent` field in the database containing an `_id` to a parent + asset. + + """ + + COLUMNS = ["label"] + Name = 0 + Deprecated = 2 + ObjectId = 3 + + DocumentRole = QtCore.Qt.UserRole + 2 + ObjectIdRole = QtCore.Qt.UserRole + 3 + + def __init__(self, parent): + super(AssetModel, self).__init__(parent=parent) + self.parent_widget = parent + self.refresh() + + @property + def db(self): + return self.parent_widget.db + + def _add_hierarchy(self, parent=None): + + # Find the assets under the parent + find_data = { + "type": "asset" + } + if parent is None: + find_data['$or'] = [ + {'data.visualParent': {'$exists': False}}, + {'data.visualParent': None} + ] + else: + find_data["data.visualParent"] = parent['_id'] + + assets = self.db.find(find_data).sort('name', 1) + for asset in assets: + # get label from data, otherwise use name + data = asset.get("data", {}) + label = data.get("label", asset['name']) + tags = data.get("tags", []) + + # store for the asset for optimization + deprecated = "deprecated" in tags + + node = Node({ + "_id": asset['_id'], + "name": asset["name"], + "label": label, + "type": asset['type'], + "tags": ", ".join(tags), + "deprecated": deprecated, + "_document": asset + }) + self.add_child(node, parent=parent) + + # Add asset's children recursively + self._add_hierarchy(node) + + def refresh(self): + """Refresh the data for the model.""" + + self.clear() + if ( + self.db.active_project() is None or + self.db.active_project() == '' + ): + return + self.beginResetModel() + self._add_hierarchy(parent=None) + self.endResetModel() + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def data(self, index, role): + + if not index.isValid(): + return + + node = index.internalPointer() + if role == QtCore.Qt.DecorationRole: # icon + + column = index.column() + if column == self.Name: + + # Allow a custom icon and custom icon color to be defined + data = node["_document"]["data"] + icon = data.get("icon", None) + color = data.get("color", style.colors.default) + + if icon is None: + # Use default icons if no custom one is specified. + # If it has children show a full folder, otherwise + # show an open folder + has_children = self.rowCount(index) > 0 + icon = "folder" if has_children else "folder-o" + + # Make the color darker when the asset is deprecated + if node.get("deprecated", False): + color = QtGui.QColor(color).darker(250) + + try: + key = "fa.{0}".format(icon) # font-awesome key + icon = awesome.icon(key, color=color) + return icon + except Exception as exception: + # Log an error message instead of erroring out completely + # when the icon couldn't be created (e.g. invalid name) + log.error(exception) + + return + + if role == QtCore.Qt.ForegroundRole: # font color + if "deprecated" in node.get("tags", []): + return QtGui.QColor(style.colors.light).darker(250) + + if role == self.ObjectIdRole: + return node.get("_id", None) + + if role == self.DocumentRole: + return node.get("_document", None) + + return super(AssetModel, self).data(index, role) diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py new file mode 100644 index 0000000000..862e4071db --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py @@ -0,0 +1,28 @@ +from . import QtCore + + +class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs) + self._filters = set() + + def setFilters(self, filters): + self._filters = set(filters) + + def filterAcceptsRow(self, source_row, source_parent): + + # No filter + if not self._filters: + return True + + else: + model = self.sourceModel() + column = self.filterKeyColumn() + idx = model.index(source_row, column, source_parent) + data = model.data(idx, self.filterRole()) + if data in self._filters: + return True + else: + return False diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py new file mode 100644 index 0000000000..04ee88229f --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py @@ -0,0 +1,30 @@ +from . import QtCore + + +class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filters to the regex if any of the children matches allow parent""" + def filterAcceptsRow(self, row, parent): + + regex = self.filterRegExp() + if not regex.isEmpty(): + pattern = regex.pattern() + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if source_index.isValid(): + + # Check current index itself + key = model.data(source_index, self.filterRole()) + if re.search(pattern, key, re.IGNORECASE): + return True + + # Check children + rows = model.rowCount(source_index) + for i in range(rows): + if self.filterAcceptsRow(i, source_index): + return True + + # Otherwise filter it + return False + + return super(RecursiveSortFilterProxyModel, + self).filterAcceptsRow(row, parent) diff --git a/pype/tools/standalonepublish/widgets/model_node.py b/pype/tools/standalonepublish/widgets/model_node.py new file mode 100644 index 0000000000..e8326d5b90 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_node.py @@ -0,0 +1,56 @@ +import logging + + +log = logging.getLogger(__name__) + + +class Node(dict): + """A node that can be represented in a tree view. + + The node can store data just like a dictionary. + + >>> data = {"name": "John", "score": 10} + >>> node = Node(data) + >>> assert node["name"] == "John" + + """ + + def __init__(self, data=None): + super(Node, self).__init__() + + self._children = list() + self._parent = None + + if data is not None: + assert isinstance(data, dict) + self.update(data) + + def childCount(self): + return len(self._children) + + def child(self, row): + + if row >= len(self._children): + log.warning("Invalid row as child: {0}".format(row)) + return + + return self._children[row] + + def children(self): + return self._children + + def parent(self): + return self._parent + + def row(self): + """ + Returns: + int: Index of this node under parent""" + if self._parent is not None: + siblings = self.parent().children() + return siblings.index(self) + + def add_child(self, child): + """Add a child to this node""" + child._parent = self + self._children.append(child) diff --git a/pype/tools/standalonepublish/widgets/model_tree.py b/pype/tools/standalonepublish/widgets/model_tree.py new file mode 100644 index 0000000000..e4f1aa5eb7 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tree.py @@ -0,0 +1,122 @@ +from . import QtCore +from . import Node + + +class TreeModel(QtCore.QAbstractItemModel): + + COLUMNS = list() + NodeRole = QtCore.Qt.UserRole + 1 + + def __init__(self, parent=None): + super(TreeModel, self).__init__(parent) + self._root_node = Node() + + def rowCount(self, parent): + if parent.isValid(): + node = parent.internalPointer() + else: + node = self._root_node + + return node.childCount() + + def columnCount(self, parent): + return len(self.COLUMNS) + + def data(self, index, role): + + if not index.isValid(): + return None + + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: + + node = index.internalPointer() + column = index.column() + + key = self.COLUMNS[column] + return node.get(key, None) + + if role == self.NodeRole: + return index.internalPointer() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + """Change the data on the nodes. + + Returns: + bool: Whether the edit was successful + """ + + if index.isValid(): + if role == QtCore.Qt.EditRole: + + node = index.internalPointer() + column = index.column() + key = self.COLUMNS[column] + node[key] = value + + # passing `list()` for PyQt5 (see PYSIDE-462) + self.dataChanged.emit(index, index, list()) + + # must return true if successful + return True + + return False + + def setColumns(self, keys): + assert isinstance(keys, (list, tuple)) + self.COLUMNS = keys + + def headerData(self, section, orientation, role): + + if role == QtCore.Qt.DisplayRole: + if section < len(self.COLUMNS): + return self.COLUMNS[section] + + super(TreeModel, self).headerData(section, orientation, role) + + def flags(self, index): + return ( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable + ) + + def parent(self, index): + + node = index.internalPointer() + parent_node = node.parent() + + # If it has no parents we return invalid + if parent_node == self._root_node or not parent_node: + return QtCore.QModelIndex() + + return self.createIndex(parent_node.row(), 0, parent_node) + + def index(self, row, column, parent): + """Return index for row/column under parent""" + + if not parent.isValid(): + parentNode = self._root_node + else: + parentNode = parent.internalPointer() + + childItem = parentNode.child(row) + if childItem: + return self.createIndex(row, column, childItem) + else: + return QtCore.QModelIndex() + + def add_child(self, node, parent=None): + if parent is None: + parent = self._root_node + + parent.add_child(node) + + def column_name(self, column): + """Return column key by index""" + + if column < len(self.COLUMNS): + return self.COLUMNS[column] + + def clear(self): + self.beginResetModel() + self._root_node = Node() + self.endResetModel() diff --git a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py new file mode 100644 index 0000000000..78bec44d36 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py @@ -0,0 +1,16 @@ +from . import QtWidgets, QtCore + + +class DeselectableTreeView(QtWidgets.QTreeView): + """A tree view that deselects on clicking on an empty area in the view""" + + def mousePressEvent(self, event): + + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + + QtWidgets.QTreeView.mousePressEvent(self, event) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py new file mode 100644 index 0000000000..665a5913a0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -0,0 +1,255 @@ +import contextlib +from . import QtWidgets, QtCore +from . import RecursiveSortFilterProxyModel, AssetModel, AssetView +from . import awesome, style + +@contextlib.contextmanager +def preserve_expanded_rows(tree_view, + column=0, + role=QtCore.Qt.DisplayRole): + """Preserves expanded row in QTreeView by column's data role. + + This function is created to maintain the expand vs collapse status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + Arguments: + tree_view (QWidgets.QTreeView): the tree view which is + nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + + model = tree_view.model() + + expanded = set() + + for index in _iter_model_rows(model, + column=column, + include_root=False): + if tree_view.isExpanded(index): + value = index.data(role) + expanded.add(value) + + try: + yield + finally: + if not expanded: + return + + for index in _iter_model_rows(model, + column=column, + include_root=False): + value = index.data(role) + state = value in expanded + if state: + tree_view.expand(index) + else: + tree_view.collapse(index) + + +@contextlib.contextmanager +def preserve_selection(tree_view, + column=0, + role=QtCore.Qt.DisplayRole, + current_index=True): + """Preserves row selection in QTreeView by column's data role. + + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + if current_index: + current_index_value = tree_view.currentIndex().data(role) + else: + current_index_value = None + + selected_rows = selection_model.selectedRows() + if not selected_rows: + yield + return + + selected = set(row.data(role) for row in selected_rows) + try: + yield + finally: + if not selected: + return + + # Go through all indices, select the ones with similar data + for index in _iter_model_rows(model, + column=column, + include_root=False): + + value = index.data(role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + if current_index_value and value == current_index_value: + tree_view.setCurrentIndex(index) + + +class AssetWidget(QtWidgets.QWidget): + """A Widget to display a tree of assets with filter + + To list the assets of the active project: + >>> # widget = AssetWidget() + >>> # widget.refresh() + >>> # widget.show() + + """ + + assets_refreshed = QtCore.Signal() # on model refresh + selection_changed = QtCore.Signal() # on view selection change + current_changed = QtCore.Signal() # on view current index change + + def __init__(self, parent): + super(AssetWidget, self).__init__(parent=parent) + self.setContentsMargins(0, 0, 0, 0) + + self.parent_widget = parent + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + # Project + self.combo_projects = QtWidgets.QComboBox() + self._set_projects() + self.combo_projects.currentTextChanged.connect(self.on_project_change) + # Tree View + model = AssetModel(self) + proxy = RecursiveSortFilterProxyModel() + proxy.setSourceModel(model) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + view = AssetView() + view.setModel(proxy) + + # Header + header = QtWidgets.QHBoxLayout() + + icon = awesome.icon("fa.refresh", color=style.colors.light) + refresh = QtWidgets.QPushButton(icon, "") + refresh.setToolTip("Refresh items") + + filter = QtWidgets.QLineEdit() + filter.textChanged.connect(proxy.setFilterFixedString) + filter.setPlaceholderText("Filter assets..") + + header.addWidget(filter) + header.addWidget(refresh) + + # Layout + layout.addWidget(self.combo_projects) + layout.addLayout(header) + layout.addWidget(view) + + # Signals/Slots + selection = view.selectionModel() + selection.selectionChanged.connect(self.selection_changed) + selection.currentChanged.connect(self.current_changed) + refresh.clicked.connect(self.refresh) + + self.refreshButton = refresh + self.model = model + self.proxy = proxy + self.view = view + + @property + def db(self): + return self.parent_widget.db + + def _set_projects(self): + projects = list() + for project in self.db.projects(): + projects.append(project['name']) + + self.combo_projects.clear() + if len(projects) > 0: + self.combo_projects.addItems(projects) + self.db.activate_project(projects[0]) + + def on_project_change(self): + projects = list() + for project in self.db.projects(): + projects.append(project['name']) + project_name = self.combo_projects.currentText() + if project_name in projects: + self.db.activate_project(project_name) + self.refresh() + + def _refresh_model(self): + self.model.refresh() + self.assets_refreshed.emit() + + def refresh(self): + self._refresh_model() + + def get_active_asset(self): + """Return the asset id the current asset.""" + current = self.view.currentIndex() + return current.data(self.model.ObjectIdRole) + + def get_active_index(self): + return self.view.currentIndex() + + def get_selected_assets(self): + """Return the assets' ids that are selected.""" + selection = self.view.selectionModel() + rows = selection.selectedRows() + return [row.data(self.model.ObjectIdRole) for row in rows] + + def select_assets(self, assets, expand=True): + """Select assets by name. + + Args: + assets (list): List of asset names + expand (bool): Whether to also expand to the asset in the view + + Returns: + None + + """ + # TODO: Instead of individual selection optimize for many assets + + assert isinstance(assets, + (tuple, list)), "Assets must be list or tuple" + + # Clear selection + selection_model = self.view.selectionModel() + selection_model.clearSelection() + + # Select + mode = selection_model.Select | selection_model.Rows + for index in _iter_model_rows(self.proxy, + column=0, + include_root=False): + data = index.data(self.model.NodeRole) + name = data['name'] + if name in assets: + selection_model.select(index, mode) + + if expand: + self.view.expand(index) + + # Set the currently active index + self.view.setCurrentIndex(index) diff --git a/pype/tools/standalonepublish/widgets/widget_asset_view.py b/pype/tools/standalonepublish/widgets/widget_asset_view.py new file mode 100644 index 0000000000..27bf374599 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_asset_view.py @@ -0,0 +1,16 @@ +from . import QtCore +from . import DeselectableTreeView + + +class AssetView(DeselectableTreeView): + """Item view. + + This implements a context menu. + + """ + + def __init__(self): + super(AssetView, self).__init__() + self.setIndentation(15) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setHeaderHidden(True) From 09cc2e9e595231dc5122a89c54047a3d53ca178d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:24:41 +0200 Subject: [PATCH 037/193] added widgets for family selection --- .../standalonepublish/widgets/__init__.py | 2 + .../widgets/widget_family.py | 290 ++++++++++++++++++ .../widgets/widget_family_desc.py | 101 ++++++ 3 files changed, 393 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_family.py create mode 100644 pype/tools/standalonepublish/widgets/widget_family_desc.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 426fa3d33c..4cf8a238e0 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -19,3 +19,5 @@ from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView from .widget_asset import AssetWidget +from .widget_family_desc import FamilyDescriptionWidget +from .widget_family import FamilyWidget diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py new file mode 100644 index 0000000000..a0786b358d --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -0,0 +1,290 @@ +import os +import sys +import inspect +import json +from collections import namedtuple + +from . import QtWidgets, QtCore, QtGui +from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import FamilyDescriptionWidget +from pype.vendor import six + +from avalon import api, io, style +from pype import lib as pypelib + + +class FamilyWidget(QtWidgets.QWidget): + + stateChanged = QtCore.Signal(bool) + data = dict() + _jobs = dict() + Separator = "---separator---" + + def __init__(self, parent): + super().__init__(parent) + # Store internal states in here + self.state = {"valid": False} + self.parent_widget = parent + + body = QtWidgets.QWidget() + lists = QtWidgets.QWidget() + + container = QtWidgets.QWidget() + + list_families = QtWidgets.QListWidget() + input_asset = QtWidgets.QLineEdit() + input_asset.setEnabled(False) + input_asset.setStyleSheet("color: #BBBBBB;") + input_subset = QtWidgets.QLineEdit() + input_result = QtWidgets.QLineEdit() + input_result.setStyleSheet("color: gray;") + input_result.setEnabled(False) + + # region Menu for default subset names + btn_subset = QtWidgets.QPushButton() + btn_subset.setFixedWidth(18) + btn_subset.setFixedHeight(20) + menu_subset = QtWidgets.QMenu(btn_subset) + btn_subset.setMenu(menu_subset) + + # endregion + name_layout = QtWidgets.QHBoxLayout() + name_layout.addWidget(input_subset) + name_layout.addWidget(btn_subset) + name_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(container) + + header = FamilyDescriptionWidget(self) + layout.addWidget(header) + + layout.addWidget(QtWidgets.QLabel("Family")) + layout.addWidget(list_families) + layout.addWidget(QtWidgets.QLabel("Asset")) + layout.addWidget(input_asset) + layout.addWidget(QtWidgets.QLabel("Subset")) + layout.addLayout(name_layout) + layout.addWidget(input_result) + layout.setContentsMargins(0, 0, 0, 0) + + options = QtWidgets.QWidget() + + layout = QtWidgets.QGridLayout(options) + layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QHBoxLayout(lists) + layout.addWidget(container) + layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(body) + layout.addWidget(lists) + layout.addWidget(options, 0, QtCore.Qt.AlignLeft) + layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + + input_subset.textChanged.connect(self.on_data_changed) + input_asset.textChanged.connect(self.on_data_changed) + list_families.currentItemChanged.connect(self.on_selection_changed) + list_families.currentItemChanged.connect(header.set_item) + + self.stateChanged.connect(self._on_state_changed) + + self.input_subset = input_subset + self.menu_subset = menu_subset + self.btn_subset = btn_subset + self.list_families = list_families + self.input_asset = input_asset + self.input_result = input_result + + self.refresh() + + @property + def db(self): + return self.parent_widget.db + + def change_asset(self, name): + self.input_asset.setText(name) + + def _on_state_changed(self, state): + self.state['valid'] = state + + def _build_menu(self, default_names): + """Create optional predefined subset names + + Args: + default_names(list): all predefined names + + Returns: + None + """ + + # Get and destroy the action group + group = self.btn_subset.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + state = any(default_names) + self.btn_subset.setEnabled(state) + if state is False: + return + + # Build new action group + group = QtWidgets.QActionGroup(self.btn_subset) + for name in default_names: + if name == self.Separator: + self.menu_subset.addSeparator() + continue + action = group.addAction(name) + self.menu_subset.addAction(action) + + group.triggered.connect(self._on_action_clicked) + + def _on_action_clicked(self, action): + self.input_subset.setText(action.text()) + + def _on_data_changed(self): + item = self.list_families.currentItem() + subset_name = self.input_subset.text() + asset_name = self.input_asset.text() + + # Get the assets from the database which match with the name + assets_db = self.db.find(filter={"type": "asset"}, projection={"name": 1}) + assets = [asset for asset in assets_db if asset_name in asset["name"]] + if item is None: + return + if assets: + # Get plugin and family + plugin = item.data(PluginRole) + if plugin is None: + return + family = plugin.family.rsplit(".", 1)[-1] + + # Get all subsets of the current asset + asset_ids = [asset["_id"] for asset in assets] + subsets = self.db.find(filter={"type": "subset", + "name": {"$regex": "{}*".format(family), + "$options": "i"}, + "parent": {"$in": asset_ids}}) or [] + + # Get all subsets' their subset name, "Default", "High", "Low" + existed_subsets = [sub["name"].split(family)[-1] + for sub in subsets] + + if plugin.defaults and isinstance(plugin.defaults, list): + defaults = plugin.defaults[:] + [self.Separator] + lowered = [d.lower() for d in plugin.defaults] + for sub in [s for s in existed_subsets + if s.lower() not in lowered]: + defaults.append(sub) + else: + defaults = existed_subsets + + self._build_menu(defaults) + + # Update the result + if subset_name: + subset_name = subset_name[0].upper() + subset_name[1:] + self.input_result.setText("{}{}".format(family, subset_name)) + + item.setData(ExistsRole, True) + self.echo("Ready ..") + else: + self._build_menu([]) + item.setData(ExistsRole, False) + if asset_name != self.parent_widget.NOT_SELECTED: + self.echo("'%s' not found .." % asset_name) + + # Update the valid state + valid = ( + subset_name.strip() != "" and + asset_name.strip() != "" and + item.data(QtCore.Qt.ItemIsEnabled) and + item.data(ExistsRole) + ) + self.stateChanged.emit(valid) + + def on_data_changed(self, *args): + + # Set invalid state until it's reconfirmed to be valid by the + # scheduled callback so any form of creation is held back until + # valid again + self.stateChanged.emit(False) + self.schedule(self._on_data_changed, 500, channel="gui") + + def on_selection_changed(self, *args): + plugin = self.list_families.currentItem().data(PluginRole) + if plugin is None: + return + + if plugin.defaults and isinstance(plugin.defaults, list): + default = plugin.defaults[0] + else: + default = "Default" + + self.input_subset.setText(default) + + self.on_data_changed() + + def keyPressEvent(self, event): + """Custom keyPressEvent. + + Override keyPressEvent to do nothing so that Maya's panels won't + take focus when pressing "SHIFT" whilst mouse is over viewport or + outliner. This way users don't accidently perform Maya commands + whilst trying to name an instance. + + """ + + def refresh(self): + has_families = False + + path_items = [ + pypelib.get_presets_path(), 'tools', 'standalone_publish.json' + ] + filepath = os.path.sep.join(path_items) + presets = dict() + with open(filepath) as data_file: + presets = json.load(data_file) + + for creator in presets.get('families', {}).values(): + creator = namedtuple("Creator", creator.keys())(*creator.values()) + + label = creator.label or creator.family + item = QtWidgets.QListWidgetItem(label) + item.setData(QtCore.Qt.ItemIsEnabled, True) + item.setData(HelpRole, creator.help or "") + item.setData(FamilyRole, creator.family) + item.setData(PluginRole, creator) + item.setData(ExistsRole, False) + self.list_families.addItem(item) + + has_families = True + + if not has_families: + item = QtWidgets.QListWidgetItem("No registered families") + item.setData(QtCore.Qt.ItemIsEnabled, False) + self.list_families.addItem(item) + + presets_path = pypelib.get_presets_path() + config_file = os.path.sep.join([presets_path, 'tools', 'creator.json']) + + self.list_families.setCurrentItem(self.list_families.item(0)) + + def echo(self, message): + if hasattr(self.parent_widget, 'echo'): + self.parent_widget.echo(message) + + def schedule(self, func, time, channel="default"): + try: + self._jobs[channel].stop() + except (AttributeError, KeyError): + pass + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(func) + timer.start(time) + + self._jobs[channel] = timer diff --git a/pype/tools/standalonepublish/widgets/widget_family_desc.py b/pype/tools/standalonepublish/widgets/widget_family_desc.py new file mode 100644 index 0000000000..e329f28ba6 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_family_desc.py @@ -0,0 +1,101 @@ +import os +import sys +import inspect +import json + +from . import QtWidgets, QtCore, QtGui +from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import awesome +from pype.vendor import six +from pype import lib as pypelib + + +class FamilyDescriptionWidget(QtWidgets.QWidget): + """A family description widget. + + Shows a family icon, family name and a help description. + Used in creator header. + + _________________ + | ____ | + | |icon| FAMILY | + | |____| help | + |_________________| + + """ + + SIZE = 35 + + def __init__(self, parent=None): + super(FamilyDescriptionWidget, self).__init__(parent=parent) + + # Header font + font = QtGui.QFont() + font.setBold(True) + font.setPointSize(14) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + icon = QtWidgets.QLabel() + icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + # Add 4 pixel padding to avoid icon being cut off + icon.setFixedWidth(self.SIZE + 4) + icon.setFixedHeight(self.SIZE + 4) + icon.setStyleSheet(""" + QLabel { + padding-right: 5px; + } + """) + + label_layout = QtWidgets.QVBoxLayout() + label_layout.setSpacing(0) + + family = QtWidgets.QLabel("family") + family.setFont(font) + family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + + help = QtWidgets.QLabel("help") + help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) + + label_layout.addWidget(family) + label_layout.addWidget(help) + + layout.addWidget(icon) + layout.addLayout(label_layout) + + self.help = help + self.family = family + self.icon = icon + + def set_item(self, item): + """Update elements to display information of a family item. + + Args: + family (dict): A family item as registered with name, help and icon + + Returns: + None + + """ + if not item: + return + + # Support a font-awesome icon + plugin = item.data(PluginRole) + icon = getattr(plugin, "icon", "info-circle") + assert isinstance(icon, six.string_types) + icon = awesome.icon("fa.{}".format(icon), color="white") + pixmap = icon.pixmap(self.SIZE, self.SIZE) + pixmap = pixmap.scaled(self.SIZE, self.SIZE) + + # Parse a clean line from the Creator's docstring + docstring = plugin.help or "" + + help = docstring.splitlines()[0] if docstring else "" + + self.icon.setPixmap(pixmap) + self.family.setText(item.data(FamilyRole)) + self.help.setText(help) From a09c90aeb3118c9e86d467b508437ad6d2d72c60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:25:19 +0200 Subject: [PATCH 038/193] added widgets for component inserting --- .../standalonepublish/widgets/__init__.py | 7 + .../widgets/widget_component.py | 189 ++++++++++++ .../widgets/widget_component_item.py | 15 + .../widgets/widget_drop_data.py | 41 +++ .../widgets/widget_drop_files.py | 273 ++++++++++++++++++ .../widgets/widget_tree_components.py | 14 + 6 files changed, 539 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_component.py create mode 100644 pype/tools/standalonepublish/widgets/widget_component_item.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_data.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_files.py create mode 100644 pype/tools/standalonepublish/widgets/widget_tree_components.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 4cf8a238e0..f4f06448a5 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -21,3 +21,10 @@ from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget +from .widget_drop_data import DropDataWidget + +from .widget_component import ComponentWidget +from .widget_tree_components import TreeComponents +from .widget_component_item import ComponentItem + +from .widget_drop_files import DropDataFrame diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py new file mode 100644 index 0000000000..f7248e31c1 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -0,0 +1,189 @@ +from . import QtCore, QtGui, QtWidgets +from . import SvgButton +from . import get_resource + + +class ComponentWidget(QtWidgets.QFrame): + C_NORMAL = '#777777' + C_HOVER = '#ffffff' + C_ACTIVE = '#4BB543' + C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + self.resize(290, 70) + self.setMinimumSize(QtCore.QSize(0, 70)) + self.parent_item = parent + # Font + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + # Main widgets + frame = QtWidgets.QFrame(self) + frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + frame.setFrameShadow(QtWidgets.QFrame.Raised) + + layout_main = QtWidgets.QHBoxLayout(frame) + layout_main.setSpacing(2) + layout_main.setContentsMargins(2, 2, 2, 2) + + # Image + Info + frame_image_info = QtWidgets.QFrame(frame) + + # Layout image info + layout = QtWidgets.QVBoxLayout(frame_image_info) + layout.setSpacing(2) + layout.setContentsMargins(2, 2, 2, 2) + + image = QtWidgets.QLabel(frame) + image.setMinimumSize(QtCore.QSize(22, 22)) + image.setMaximumSize(QtCore.QSize(22, 22)) + image.setText("") + image.setScaledContents(True) + pixmap = QtGui.QPixmap(get_resource('image_sequence.png')) + image.setPixmap(pixmap) + + self.info = SvgButton( + get_resource('information.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame_image_info, False + ) + + expanding_sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) + expanding_sizePolicy.setHorizontalStretch(0) + expanding_sizePolicy.setVerticalStretch(0) + + layout.addWidget(image, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + + layout_main.addWidget(frame_image_info) + + # Name + representation + self.name = QtWidgets.QLabel(frame) + self.frames = QtWidgets.QLabel(frame) + self.ext = QtWidgets.QLabel(frame) + + self.name.setFont(font) + self.frames.setFont(font) + self.ext.setFont(font) + + self.frames.setStyleSheet('padding-left:3px;') + + expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) + + frame_name_repre = QtWidgets.QFrame(frame) + + self.frames.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + layout = QtWidgets.QHBoxLayout(frame_name_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.frames, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) + + frame_name_repre.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Frames + icons + frame_repre_icons = QtWidgets.QFrame(frame) + + label_repre = QtWidgets.QLabel() + label_repre.setText('Representation:') + + self.input_repre = QtWidgets.QLineEdit() + self.input_repre.setMaximumWidth(50) + + frame_icons = QtWidgets.QFrame(frame_repre_icons) + + self.preview = SvgButton( + get_resource('preview.svg'), 64, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + self.thumbnail = SvgButton( + get_resource('thumbnail.svg'), 84, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + layout = QtWidgets.QHBoxLayout(frame_icons) + layout.setSpacing(6) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.thumbnail) + layout.addWidget(self.preview) + + layout = QtWidgets.QHBoxLayout(frame_repre_icons) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) + + frame_middle = QtWidgets.QFrame(frame) + + layout = QtWidgets.QVBoxLayout(frame_middle) + layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.addWidget(frame_name_repre) + layout.addWidget(frame_repre_icons) + + layout.setStretchFactor(frame_name_repre, 1) + layout.setStretchFactor(frame_repre_icons, 1) + + layout_main.addWidget(frame_middle) + + self.remove = SvgButton( + get_resource('trash.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame, False + ) + + layout_main.addWidget(self.remove) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + layout.addWidget(frame) + + self.preview.setToolTip('Mark component as Preview') + self.thumbnail.setToolTip('Component will be selected as thumbnail') + + # self.frame.setStyleSheet("border: 1px solid black;") + + def set_context(self, data): + + self.remove.clicked.connect(self._remove) + name = data['name'] + representation = data['representation'] + ext = data['ext'] + file_info = data['file_info'] + thumb = data['thumb'] + prev = data['prev'] + info = data['info'] + + self.name.setText(name) + self.input_repre.setText(representation) + self.ext.setText('( {} )'.format(ext)) + if file_info is None: + self.file_info.setVisible(False) + else: + self.file_info.setText('[{}]'.format(file_info)) + + # self.thumbnail.setVisible(thumb) + # self.preview.setVisible(prev) + + def _remove(self): + self.signal_remove.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py new file mode 100644 index 0000000000..1236a439c0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -0,0 +1,15 @@ +from . import QtWidgets +from . import ComponentWidget + + +class ComponentItem(QtWidgets.QTreeWidgetItem): + def __init__(self, parent, data): + super().__init__(parent) + self.in_data = data + self._widget = ComponentWidget(self) + self._widget.set_context(data) + + self.treeWidget().setItemWidget(self, 0, self._widget) + + def double_clicked(*args): + pass diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py new file mode 100644 index 0000000000..96294ea99e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_data.py @@ -0,0 +1,41 @@ +import os +import logging +import clique +from . import QtWidgets, QtCore, QtGui + + +class DropDataWidget(QtWidgets.QWidget): + + def __init__(self, parent): + '''Initialise DataDropZone widget.''' + super().__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + + bottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter + topCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + + self._label = QtWidgets.QLabel('Drop files here') + layout.addWidget( + self._label, + alignment=bottomCenterAlignment + ) + + self._browseButton = QtWidgets.QPushButton('Browse') + self._browseButton.setToolTip('Browse for file(s).') + layout.addWidget( + self._browseButton, alignment=topCenterAlignment + ) + + def paintEvent(self, event): + super().paintEvent(event) + painter = QtGui.QPainter(self) + pen = QtGui.QPen() + pen.setWidth(1); + pen.setBrush(QtCore.Qt.darkGray); + pen.setStyle(QtCore.Qt.DashLine); + painter.setPen(pen) + painter.drawRect( + 10, 10, + self.rect().width()-15, self.rect().height()-15 + ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_files.py new file mode 100644 index 0000000000..0b2241e465 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_files.py @@ -0,0 +1,273 @@ +import os +import clique +from . import QtWidgets, QtCore +from . import ComponentItem, TreeComponents, DropDataWidget + + +class DropDataFrame(QtWidgets.QFrame): + # signal_dropped = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + + self.items = [] + + self.setAcceptDrops(True) + layout = QtWidgets.QVBoxLayout(self) + + self.tree_widget = TreeComponents(self) + layout.addWidget(self.tree_widget) + + self.drop_widget = DropDataWidget(self) + layout.addWidget(self.drop_widget) + + self._refresh_view() + + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + paths = self._processMimeData(event.mimeData()) + if paths: + self._add_components(paths) + event.accept() + + def _processMimeData(self, mimeData): + paths = [] + + if not mimeData.hasUrls(): + print('Dropped invalid file/folder') + return paths + + for path in mimeData.urls(): + local_path = path.toLocalFile() + if os.path.isfile(local_path) or os.path.isdir(local_path): + paths.append(local_path) + else: + print('Invalid input: "{}"'.format(local_path)) + + return paths + + def _add_components(self, paths): + components = self._process_paths(paths) + if not components: + return + for component in components: + self._add_item(component) + + def _add_item(self, data): + # Assign to self so garbage collector wont remove the component + # during initialization + self.new_component = ComponentItem(self.tree_widget, data) + self.new_component._widget.signal_remove.connect(self._remove_item) + self.tree_widget.addTopLevelItem(self.new_component) + self.items.append(self.new_component) + self.new_component = None + + self._refresh_view() + + def _remove_item(self, item): + root = self.tree_widget.invisibleRootItem() + (item.parent() or root).removeChild(item) + self.items.remove(item) + self._refresh_view() + + def _refresh_view(self): + _bool = len(self.items) == 0 + + self.tree_widget.setVisible(not _bool) + self.drop_widget.setVisible(_bool) + + def _process_paths(self, in_paths): + paths = self._get_all_paths(in_paths) + collections, remainders = clique.assemble(paths) + for collection in collections: + self._process_collection(collection) + for remainder in remainders: + self._process_remainder(remainder) + + def _get_all_paths(self, paths): + output_paths = [] + for path in paths: + path = os.path.normpath(path) + if os.path.isfile(path): + output_paths.append(path) + elif os.path.isdir(path): + s_paths = [] + for s_item in os.listdir(path): + s_path = os.path.sep.join([path, s_item]) + s_paths.append(s_path) + output_paths.extend(self._get_all_paths(s_paths)) + else: + print('Invalid path: "{}"'.format(path)) + return output_paths + + def _process_collection(self, collection): + file_base = os.path.basename(collection.head) + folder_path = os.path.dirname(collection.head) + if file_base[-1] in ['.']: + file_base = file_base[:-1] + file_ext = collection.tail + repr_name = file_ext.replace('.', '') + range = self._get_ranges(collection.indexes) + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + for file in os.listdir(folder_path): + if file.startswith(file_base) and file.endswith(file_ext): + files.append(os.path.sep.join([folder_path, file])) + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': range, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': True, + 'info': info + } + self._process_data(data) + + def _get_ranges(self, indexes): + if len(indexes) == 1: + return str(indexes[0]) + ranges = [] + first = None + last = None + for index in indexes: + if first is None: + first = index + last = index + elif (last+1) == index: + last = index + else: + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + first = index + last = index + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + return ', '.join(ranges) + + def _process_remainder(self, remainder): + filename = os.path.basename(remainder) + folder_path = os.path.dirname(remainder) + file_base, file_ext = os.path.splitext(filename) + repr_name = file_ext.replace('.', '') + file_info = None + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + files.append(remainder) + + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': file_info, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': False, + 'info': info + } + + self._process_data(data) + + def _process_data(self, data): + found = False + for item in self.items: + if data['ext'] != item.in_data['ext']: + continue + if data['folder_path'] != item.in_data['folder_path']: + continue + + new_is_seq = data['is_sequence'] + ex_is_seq = item.in_data['is_sequence'] + + # If both are single files + if not new_is_seq and not ex_is_seq: + if data['name'] != item.in_data['name']: + continue + found = True + break + # If new is sequence and ex is single file + elif new_is_seq and not ex_is_seq: + if data['name'] not in item.in_data['name']: + continue + ex_file = item.in_data['files'][0] + found = True + # If file is one of inserted sequence + if ex_file in data['files']: + self._remove_item(item) + self._add_item(data) + break + # if file is missing in inserted sequence + paths = data['files'] + paths.append(ex_file) + collections, remainders = clique.assemble(paths) + self._process_collection(collections[0]) + break + # If new is single file existing is sequence + elif not new_is_seq and ex_is_seq: + if item.in_data['name'] not in data['name']: + continue + new_file = data['files'][0] + found = True + if new_file in item.in_data['files']: + break + paths = item.in_data['files'] + paths.append(new_file) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + + break + # If both are sequence + else: + if data['name'] != item.in_data['name']: + continue + found = True + ex_files = item.in_data['files'] + for file in data['files']: + if file not in ex_files: + ex_files.append(file) + paths = list(set(ex_files)) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + break + + if found is False: + self._add_item(data) diff --git a/pype/tools/standalonepublish/widgets/widget_tree_components.py b/pype/tools/standalonepublish/widgets/widget_tree_components.py new file mode 100644 index 0000000000..76e5a9bce0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_tree_components.py @@ -0,0 +1,14 @@ +from . import QtCore, QtGui, QtWidgets + + +class TreeComponents(QtWidgets.QTreeWidget): + def __init__(self, parent): + super().__init__(parent) + + self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled) + self.setIndentation(28) + self.headerItem().setText(0, 'Components') + + self.setRootIsDecorated(False) + + self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c)) From 01b34cebd3eb07946f3b7fddc6c21d739d5260a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:25:38 +0200 Subject: [PATCH 039/193] added basic app --- pype/tools/standalonepublish/app.py | 155 ++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pype/tools/standalonepublish/app.py diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py new file mode 100644 index 0000000000..ff548db30f --- /dev/null +++ b/pype/tools/standalonepublish/app.py @@ -0,0 +1,155 @@ +import os +import sys +import json +from subprocess import Popen +from pype import lib as pypelib +from avalon.vendor.Qt import QtWidgets, QtCore +from avalon import api, style, schema +from avalon.tools import lib as parentlib +from .widgets import * +# Move this to pype lib? +from avalon.tools.libraryloader.io_nonsingleton import DbConnector + + +class Window(QtWidgets.QDialog): + _db = DbConnector() + _jobs = {} + WIDTH = 1000 + HEIGHT = 500 + NOT_SELECTED = '< Nothing is selected >' + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + self._db.install() + + self.setWindowTitle("Standalone Publish") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setStyleSheet(style.load_stylesheet()) + + # Validators + self.valid_parent = False + + # statusbar - added under asset_widget + label_message = QtWidgets.QLabel() + label_message.setFixedHeight(20) + + # assets widget + widget_assets_wrap = QtWidgets.QWidget() + widget_assets_wrap.setContentsMargins(0, 0, 0, 0) + widget_assets = AssetWidget(self) + + layout_assets = QtWidgets.QVBoxLayout(widget_assets_wrap) + layout_assets.addWidget(widget_assets) + layout_assets.addWidget(label_message) + + + # family widget + widget_family = FamilyWidget(self) + + # components widget + widget_components = DropDataFrame(self) + + # Body + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + body.setOrientation(QtCore.Qt.Horizontal) + body.addWidget(widget_assets_wrap) + body.addWidget(widget_family) + body.addWidget(widget_components) + body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) + body.setStretchFactor(body.indexOf(widget_family), 2) + body.setStretchFactor(body.indexOf(widget_components), 3) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + + self.resize(self.WIDTH, self.HEIGHT) + + # signals + widget_assets.selection_changed.connect(self.on_asset_changed) + + self.label_message = label_message + self.widget_assets = widget_assets + self.widget_family = widget_family + + self.echo("Connected to Database") + + # on start + self.on_start() + + @property + def db(self): + return self._db + + def on_start(self): + # Refresh asset input in Family widget + self.on_asset_changed() + + def get_avalon_parent(self, entity): + parent_id = entity['data']['visualParent'] + parents = [] + if parent_id is not None: + parent = self.db.find_one({'_id': parent_id}) + parents.extend(self.get_avalon_parent(parent)) + parents.append(parent['name']) + return parents + + def echo(self, message): + self.label_message.setText(str(message)) + QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) + + def on_asset_changed(self): + """Callback on asset selection changed + + This updates the task view. + + """ + selected = self.widget_assets.get_selected_assets() + if len(selected) == 1: + self.valid_parent = True + asset = self.db.find_one({"_id": selected[0], "type": "asset"}) + self.widget_family.change_asset(asset['name']) + else: + self.valid_parent = False + self.widget_family.change_asset(self.NOT_SELECTED) + self.widget_family.on_data_changed() + + +def show(parent=None, debug=False, context=None): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + with parentlib.application(): + window = Window(parent, context) + window.show() + + module.window = window + + +def cli(args): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("project") + parser.add_argument("asset") + + args = parser.parse_args(args) + # project = args.project + # asset = args.asset + + show() From ba47be80874b5e3fddf457876eac6cde5e77bc52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:28:28 +0200 Subject: [PATCH 040/193] added forgotten Task model in asset widgets --- .../standalonepublish/widgets/__init__.py | 1 + .../widgets/model_tasks_template.py | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/model_tasks_template.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index f4f06448a5..94811ab298 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -15,6 +15,7 @@ from .model_tree import TreeModel from .model_asset import AssetModel from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel +from .model_tasks_template import TasksTemplateModel from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView diff --git a/pype/tools/standalonepublish/widgets/model_tasks_template.py b/pype/tools/standalonepublish/widgets/model_tasks_template.py new file mode 100644 index 0000000000..4af3b9eea7 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tasks_template.py @@ -0,0 +1,65 @@ +from . import QtCore, TreeModel +from . import Node +from . import awesome, style + + +class TasksTemplateModel(TreeModel): + """A model listing the tasks combined for a list of assets""" + + COLUMNS = ["Tasks"] + + def __init__(self): + super(TasksTemplateModel, self).__init__() + self.selectable = False + self._icons = { + "__default__": awesome.icon("fa.folder-o", + color=style.colors.default) + } + + def set_tasks(self, tasks): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + + """ + + self.clear() + + # let cleared task view if no tasks are available + if len(tasks) == 0: + return + + self.beginResetModel() + + icon = self._icons["__default__"] + for task in tasks: + node = Node({ + "Tasks": task, + "icon": icon + }) + + self.add_child(node) + + self.endResetModel() + + def flags(self, index): + if self.selectable is False: + return QtCore.Qt.ItemIsEnabled + else: + return ( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable + ) + + def data(self, index, role): + + if not index.isValid(): + return + + # Add icon to the first column + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + return index.internalPointer()['icon'] + + return super(TasksTemplateModel, self).data(index, role) From b6a1fb8209043f5b510adb0aba4f58eac21f31db Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:42:30 +0200 Subject: [PATCH 041/193] added icons to resources --- .../tools/standalonepublish/resources/file.png | Bin 0 -> 803 bytes .../standalonepublish/resources/files.png | Bin 0 -> 484 bytes .../standalonepublish/resources/houdini.png | Bin 0 -> 262950 bytes .../standalonepublish/resources/image_file.png | Bin 0 -> 5118 bytes .../resources/image_files.png | Bin 0 -> 8560 bytes .../resources/image_sequence.png | Bin 5092 -> 0 bytes .../tools/standalonepublish/resources/maya.png | Bin 0 -> 41557 bytes .../tools/standalonepublish/resources/nuke.png | Bin 0 -> 49012 bytes .../standalonepublish/resources/premiere.png | Bin 0 -> 20121 bytes .../standalonepublish/resources/video_file.png | Bin 0 -> 120 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pype/tools/standalonepublish/resources/file.png create mode 100644 pype/tools/standalonepublish/resources/files.png create mode 100644 pype/tools/standalonepublish/resources/houdini.png create mode 100644 pype/tools/standalonepublish/resources/image_file.png create mode 100644 pype/tools/standalonepublish/resources/image_files.png delete mode 100644 pype/tools/standalonepublish/resources/image_sequence.png create mode 100644 pype/tools/standalonepublish/resources/maya.png create mode 100644 pype/tools/standalonepublish/resources/nuke.png create mode 100644 pype/tools/standalonepublish/resources/premiere.png create mode 100644 pype/tools/standalonepublish/resources/video_file.png diff --git a/pype/tools/standalonepublish/resources/file.png b/pype/tools/standalonepublish/resources/file.png new file mode 100644 index 0000000000000000000000000000000000000000..7a830ad13305a4dbaf73489ed9f373f5382243c0 GIT binary patch literal 803 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!+2l#}z0_p!F3`DHD_7v#tfRZ4; zUDcV|DfRTh`5BL%&h#P@`jH7S&J4gS-O11s&(r( zY}~YY%hnw`_w2oP{l?8(cke%Z@%qh&PhY-$|MC0J-+!HdPj>>%W=!&ScL{i$=d%aM zZt`?-49U3n_SW$rCPx9b2XT?iOi5zKO`ObHT}elJx(@C6f4So3%AoQaFB{m`zI$Er zH~#B85t%B+ec}6V?5Ht0p~bv)*(zD*m8D7l{&Xvf<+cS~pK)JAO!DWHq;K7XCcnQmtP_R_Bv-WZSWV&Q5R@v)N*{l zAdvGoUz@vOL6cYo1B-P;Bd5cLZjl`fEY%i`ybiAzR6j6!$R@EcGCUM?n!q4%?N1X& z!-pH%Cm1;VYnlWaHoOyD!oV|6jXQ~HCBtEHPbPK`Emn~q?-^dN4`*Z*32;Co z8tqwcr2gp8sW<7$JnHvmRiPi7j*H>-jl#Qi_MYY;Gp00i_>zopr0D++3 A5&!@I literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/files.png b/pype/tools/standalonepublish/resources/files.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f89fe14992cde5d9509c2606efe946c1a9e95c GIT binary patch literal 484 zcmVsb@n$`BycGxbTj??esuHn0x-zMy6fcmeDIGr%

n4igvKbDOy^aUL%8G~`^%3w6m;|=edlB&^ zXNJwBvu=~lO3zkHdwf;99pEl7)#UUwJA4JK72!=_BQ?l2@SO9P+(wIP`i&iPew~vk zY&9bG)s>d8F7U7_nZg>Gp`_2i?TXw7U?B%3*0#;XJ*cdj3RcheW zuaepUuJz+O6Xdn}z9nxSxHSl7X*~-(Z3+8FRoA%7|9|XZ^apsG?JOdGsCR(tBZBoJ z;#&&q0!Ia~N5v%1Ne$pJa0NJwh)-!v3LC}@3&3)*-U70)OEE)E9WKcXxkLb{R~fMx aa*4m35Kv^)H)-Dh00008qo>> literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/houdini.png b/pype/tools/standalonepublish/resources/houdini.png new file mode 100644 index 0000000000000000000000000000000000000000..11cfa46dce2afe609a525b9b741f612445d8fe98 GIT binary patch literal 262950 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 l89UyV%&>*Q|1Nzx^yCixAVVXrD*fNYN^>s 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 diff --git a/pype/tools/standalonepublish/resources/maya.png b/pype/tools/standalonepublish/resources/maya.png new file mode 100644 index 0000000000000000000000000000000000000000..e84a6a3742f325769d6cf619045d17107d925f3c GIT binary patch literal 41557 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 literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/nuke.png b/pype/tools/standalonepublish/resources/nuke.png new file mode 100644 index 0000000000000000000000000000000000000000..423445409618d59ff822dc09e1dac2e4d3e241aa GIT binary patch literal 49012 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

n)d3@oZpYFsmbJyGIDGW; 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 R3jP~ZQC3Z+PRcy&{{SdSemwvH literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/premiere.png b/pype/tools/standalonepublish/resources/premiere.png new file mode 100644 index 0000000000000000000000000000000000000000..eb5b3d1ba2adb45f6feb2c35f19bc1bc49a22b93 GIT binary patch literal 20121 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 Date: Wed, 17 Apr 2019 09:44:30 +0200 Subject: [PATCH 042/193] icons are loaded to widget --- .../standalonepublish/widgets/__init__.py | 2 ++ .../widgets/widget_component.py | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 94811ab298..21f703d762 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -29,3 +29,5 @@ from .widget_tree_components import TreeComponents from .widget_component_item import ComponentItem from .widget_drop_files import DropDataFrame + +from .widget_components import ComponentsWidget diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index f7248e31c1..5fa5da343c 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -40,13 +40,11 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(2) layout.setContentsMargins(2, 2, 2, 2) - image = QtWidgets.QLabel(frame) - image.setMinimumSize(QtCore.QSize(22, 22)) - image.setMaximumSize(QtCore.QSize(22, 22)) - image.setText("") - image.setScaledContents(True) - pixmap = QtGui.QPixmap(get_resource('image_sequence.png')) - image.setPixmap(pixmap) + self.icon = QtWidgets.QLabel(frame) + self.icon.setMinimumSize(QtCore.QSize(22, 22)) + self.icon.setMaximumSize(QtCore.QSize(22, 22)) + self.icon.setText("") + self.icon.setScaledContents(True) self.info = SvgButton( get_resource('information.svg'), 22, 22, @@ -60,7 +58,7 @@ class ComponentWidget(QtWidgets.QFrame): expanding_sizePolicy.setHorizontalStretch(0) expanding_sizePolicy.setVerticalStretch(0) - layout.addWidget(image, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -173,6 +171,20 @@ class ComponentWidget(QtWidgets.QFrame): thumb = data['thumb'] prev = data['prev'] info = data['info'] + icon = data['icon'] + + resource = None + if icon is not None: + resource = get_resource('{}.png'.format(icon)) + + if resource is None or not os.path.isfile(resource): + if data['is_sequence']: + resource = get_resource('files.png') + else: + resource = get_resource('file.png') + + pixmap = QtGui.QPixmap(resource) + self.icon.setPixmap(pixmap) self.name.setText(name) self.input_repre.setText(representation) From ffa7f7b5dbf5454abec3f4514d7445adc27e1201 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:11 +0200 Subject: [PATCH 043/193] added components wiget that holds drop frame and browse and publish buttons --- .../widgets/widget_components.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py new file mode 100644 index 0000000000..18d4e480d5 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -0,0 +1,76 @@ +from . import QtWidgets, QtCore, QtGui +from . import DropDataFrame + +class ComponentsWidget(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + body = QtWidgets.QWidget() + self.parent_widget = parent + self.drop_frame = DropDataFrame(self) + + buttons = QtWidgets.QWidget() + + layout = QtWidgets.QHBoxLayout(buttons) + + self.btn_browse = QtWidgets.QPushButton('Browse') + self.btn_browse.setToolTip('Browse for file(s).') + + self.btn_publish = QtWidgets.QPushButton('Publish') + self.btn_publish.setToolTip('Publishes data.') + + layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight) + + layout = QtWidgets.QVBoxLayout(body) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.drop_frame) + layout.addWidget(buttons) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(body) + + self.btn_browse.clicked.connect(self._browse) + # self.setStyleSheet("border: 1px solid black;") + + def set_valid(self, in_bool): + self.btn_publish.setEnabled(in_bool) + + def set_valid_components(self, in_bool): + self.parent_widget.set_valid_components(in_bool) + + def _browse(self): + options = [ + QtWidgets.QFileDialog.DontResolveSymlinks, + QtWidgets.QFileDialog.DontUseNativeDialog + ] + folders = False + if folders: + # browse folders specifics + caption = "Browse folders to publish image sequences" + file_mode = QtWidgets.QFileDialog.Directory + options.append(QtWidgets.QFileDialog.ShowDirsOnly) + else: + # browse files specifics + caption = "Browse files to publish" + file_mode = QtWidgets.QFileDialog.ExistingFiles + + # create the dialog + file_dialog = QtWidgets.QFileDialog(parent=self, caption=caption) + file_dialog.setLabelText(QtWidgets.QFileDialog.Accept, "Select") + file_dialog.setLabelText(QtWidgets.QFileDialog.Reject, "Cancel") + file_dialog.setFileMode(file_mode) + + # set the appropriate options + for option in options: + file_dialog.setOption(option) + + # browse! + if not file_dialog.exec_(): + return + + # process the browsed files/folders for publishing + paths = file_dialog.selectedFiles() + self.drop_frame._process_paths(paths) From 9a827fbaaa03c9b3eb86bc47354cab11e362e4b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:39 +0200 Subject: [PATCH 044/193] assets widget can collect data --- pype/tools/standalonepublish/widgets/widget_asset.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 665a5913a0..82b3700dea 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -178,6 +178,13 @@ class AssetWidget(QtWidgets.QWidget): def db(self): return self.parent_widget.db + def collect_data(self): + data = { + 'project': self.combo_projects.currentText(), + 'asset': get_active_asset + } + return data + def _set_projects(self): projects = list() for project in self.db.projects(): From a2776d967ab3272a672e2ab71acd7d2763f4dab5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:54 +0200 Subject: [PATCH 045/193] family widget can collect data --- pype/tools/standalonepublish/widgets/widget_family.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index a0786b358d..4ef7d9bcf8 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -100,6 +100,15 @@ class FamilyWidget(QtWidgets.QWidget): self.refresh() + def collect_data(self): + plugin = self.list_families.currentItem().data(PluginRole) + family = plugin.family.rsplit(".", 1)[-1] + data = { + 'family': family, + 'subset': self.input_subset.text() + } + return data + @property def db(self): return self.parent_widget.db @@ -109,6 +118,7 @@ class FamilyWidget(QtWidgets.QWidget): def _on_state_changed(self, state): self.state['valid'] = state + self.parent_widget.set_valid_family(state) def _build_menu(self, default_names): """Create optional predefined subset names From cfacb279d7eaf5c236c411dbe29bad5e7d8328b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:48:42 +0200 Subject: [PATCH 046/193] added validations into main window --- pype/tools/standalonepublish/app.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index ff548db30f..2edc345cbb 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -14,7 +14,10 @@ from avalon.tools.libraryloader.io_nonsingleton import DbConnector class Window(QtWidgets.QDialog): _db = DbConnector() _jobs = {} - WIDTH = 1000 + valid_family = False + valid_components = False + initialized = False + WIDTH = 1100 HEIGHT = 500 NOT_SELECTED = '< Nothing is selected >' @@ -76,6 +79,7 @@ class Window(QtWidgets.QDialog): self.label_message = label_message self.widget_assets = widget_assets self.widget_family = widget_family + self.widget_components = widget_components self.echo("Connected to Database") @@ -87,8 +91,10 @@ class Window(QtWidgets.QDialog): return self._db def on_start(self): + self.initialized = True # Refresh asset input in Family widget self.on_asset_changed() + self.validation() def get_avalon_parent(self, entity): parent_id = entity['data']['visualParent'] @@ -119,6 +125,26 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(self.NOT_SELECTED) self.widget_family.on_data_changed() + def validation(self): + if not self.initialized: + return + valid = self.valid_family and self.valid_components + self.widget_components.set_valid(valid) + + def set_valid_family(self, valid): + self.valid_family = valid + self.validation() + + def set_valid_components(self, valid): + self.valid_components = valid + self.validation() + + def collect_data(self): + data = {} + data.update(self.widget_assets.collect_data()) + data.update(self.widget_family.collect_data()) + + return data def show(parent=None, debug=False, context=None): """Display Loader GUI From fe3d94c203c7dac17a191ee8fb41cb1b782dc43c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:53:20 +0200 Subject: [PATCH 047/193] component item buttons are working now --- .../widgets/widget_component.py | 16 +++- .../widgets/widget_drop_files.py | 75 ++++++++++++------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index 5fa5da343c..2e65bcf1d8 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -1,3 +1,4 @@ +import os from . import QtCore, QtGui, QtWidgets from . import SvgButton from . import get_resource @@ -9,6 +10,8 @@ class ComponentWidget(QtWidgets.QFrame): C_ACTIVE = '#4BB543' C_ACTIVE_HOVER = '#4BF543' signal_remove = QtCore.Signal(object) + signal_thumbnail = QtCore.Signal(object) + signal_preview = QtCore.Signal(object) def __init__(self, parent): super().__init__() @@ -162,8 +165,9 @@ class ComponentWidget(QtWidgets.QFrame): # self.frame.setStyleSheet("border: 1px solid black;") def set_context(self, data): - self.remove.clicked.connect(self._remove) + self.thumbnail.clicked.connect(self._thumbnail_clicked) + self.preview.clicked.connect(self._preview_clicked) name = data['name'] representation = data['representation'] ext = data['ext'] @@ -194,8 +198,14 @@ class ComponentWidget(QtWidgets.QFrame): else: self.file_info.setText('[{}]'.format(file_info)) - # self.thumbnail.setVisible(thumb) - # self.preview.setVisible(prev) + self.thumbnail.setVisible(thumb) + self.preview.setVisible(prev) def _remove(self): self.signal_remove.emit(self.parent_item) + + def _thumbnail_clicked(self): + self.signal_thumbnail.emit(self.parent_item) + + def _preview_clicked(self): + self.signal_preview.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_files.py index 0b2241e465..fbeddcbb41 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_files.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_files.py @@ -1,20 +1,19 @@ import os import clique +from pypeapp import config from . import QtWidgets, QtCore from . import ComponentItem, TreeComponents, DropDataWidget class DropDataFrame(QtWidgets.QFrame): - # signal_dropped = QtCore.Signal(object) - def __init__(self, parent): super().__init__() - + self.parent_widget = parent self.items = [] + self.presets = config.get_presets()['tools']['standalone_publish'] self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) - self.tree_widget = TreeComponents(self) layout.addWidget(self.tree_widget) @@ -63,13 +62,43 @@ class DropDataFrame(QtWidgets.QFrame): # Assign to self so garbage collector wont remove the component # during initialization self.new_component = ComponentItem(self.tree_widget, data) - self.new_component._widget.signal_remove.connect(self._remove_item) self.tree_widget.addTopLevelItem(self.new_component) + self.new_component.set_context() + + self.new_component._widget.signal_remove.connect(self._remove_item) + self.new_component._widget.signal_preview.connect(self._set_preview) + self.new_component._widget.signal_thumbnail.connect( + self._set_thumbnail + ) self.items.append(self.new_component) self.new_component = None self._refresh_view() + def _set_thumbnail(self, in_item): + checked_item = None + for item in self.items: + if item.is_thumbnail(): + checked_item = item + break + if checked_item is None or checked_item == in_item: + in_item.change_thumbnail() + else: + checked_item.change_thumbnail(False) + in_item.change_thumbnail() + + def _set_preview(self, in_item): + checked_item = None + for item in self.items: + if item.is_preview(): + checked_item = item + break + if checked_item is None or checked_item == in_item: + in_item.change_preview() + else: + checked_item.change_preview(False) + in_item.change_preview() + def _remove_item(self, item): root = self.tree_widget.invisibleRootItem() (item.parent() or root).removeChild(item) @@ -78,10 +107,11 @@ class DropDataFrame(QtWidgets.QFrame): def _refresh_view(self): _bool = len(self.items) == 0 - self.tree_widget.setVisible(not _bool) self.drop_widget.setVisible(_bool) + self.parent_widget.set_valid_components(not _bool) + def _process_paths(self, in_paths): paths = self._get_all_paths(in_paths) collections, remainders = clique.assemble(paths) @@ -114,13 +144,6 @@ class DropDataFrame(QtWidgets.QFrame): file_ext = collection.tail repr_name = file_ext.replace('.', '') range = self._get_ranges(collection.indexes) - thumb = False - if file_ext in ['.jpeg']: - thumb = True - - prev = False - if file_ext in ['.jpeg']: - prev = True files = [] for file in os.listdir(folder_path): @@ -135,9 +158,6 @@ class DropDataFrame(QtWidgets.QFrame): 'file_info': range, 'representation': repr_name, 'folder_path': folder_path, - 'icon': 'sequence', - 'thumb': thumb, - 'prev': prev, 'is_sequence': True, 'info': info } @@ -176,13 +196,6 @@ class DropDataFrame(QtWidgets.QFrame): file_base, file_ext = os.path.splitext(filename) repr_name = file_ext.replace('.', '') file_info = None - thumb = False - if file_ext in ['.jpeg']: - thumb = True - - prev = False - if file_ext in ['.jpeg']: - prev = True files = [] files.append(remainder) @@ -196,9 +209,6 @@ class DropDataFrame(QtWidgets.QFrame): 'file_info': file_info, 'representation': repr_name, 'folder_path': folder_path, - 'icon': 'sequence', - 'thumb': thumb, - 'prev': prev, 'is_sequence': False, 'info': info } @@ -206,6 +216,19 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def _process_data(self, data): + ext = data['ext'] + icon = 'default' + for ico, exts in self.presets['extensions'].items(): + if ext in exts: + icon = ico + break + # Add 's' to icon_name if is sequence (image -> images) + if data['is_sequence']: + icon += 's' + data['icon'] = icon + data['thumb'] = ext in self.presets['thumbnailable'] + data['prev'] = ext in self.presets['extensions']['video_file'] + found = False for item in self.items: if data['ext'] != item.in_data['ext']: From b2b4c031c143a938b4aa6c0674827a7ca87d7dc9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:08 +0200 Subject: [PATCH 048/193] component item can coverup component widget changes --- .../widgets/widget_component_item.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index 1236a439c0..dcf66a9c21 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -5,11 +5,25 @@ from . import ComponentWidget class ComponentItem(QtWidgets.QTreeWidgetItem): def __init__(self, parent, data): super().__init__(parent) + self.parent_widget = parent self.in_data = data - self._widget = ComponentWidget(self) - self._widget.set_context(data) + def set_context(self): + self._widget = ComponentWidget(self) + self._widget.set_context(self.in_data) self.treeWidget().setItemWidget(self, 0, self._widget) + def is_thumbnail(self): + return self._widget.thumbnail.checked + + def change_thumbnail(self, hover=True): + self._widget.thumbnail.change_checked(hover) + + def is_preview(self): + return self._widget.preview.checked + + def change_preview(self, hover=True): + self._widget.preview.change_checked(hover) + def double_clicked(*args): pass From b3c15a16c93a2b39dec1d0a222eebc873fdd5d6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:29 +0200 Subject: [PATCH 049/193] removed browse button from drop zone --- .../standalonepublish/widgets/widget_drop_data.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py index 96294ea99e..74e30b4714 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_data.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_data.py @@ -12,19 +12,12 @@ class DropDataWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) - bottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter - topCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + CenterAlignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter self._label = QtWidgets.QLabel('Drop files here') layout.addWidget( self._label, - alignment=bottomCenterAlignment - ) - - self._browseButton = QtWidgets.QPushButton('Browse') - self._browseButton.setToolTip('Browse for file(s).') - layout.addWidget( - self._browseButton, alignment=topCenterAlignment + alignment=CenterAlignment ) def paintEvent(self, event): From 574c9ef66e9ce1de2d3811c26d463f0e78ecdc13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:52 +0200 Subject: [PATCH 050/193] svg button works as it should work --- .../standalonepublish/widgets/button_from_svgs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py index aeeeae5c7a..3a35bcd838 100644 --- a/pype/tools/standalonepublish/widgets/button_from_svgs.py +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -87,9 +87,13 @@ class SvgButton(QtWidgets.QFrame): self.mousePressEvent(event) return False - def change_checked(self, in_bool=False): + def change_checked(self, hover=True): if self.checkable: - self.checked = in_bool + self.checked = not self.checked + if hover: + self.hoverEnterEvent() + else: + self.hoverLeaveEvent() def hoverEnterEvent(self, event=None): color = self.c_hover @@ -104,6 +108,4 @@ class SvgButton(QtWidgets.QFrame): self.svg_widget.change_color(color) def mousePressEvent(self, event=None): - self.change_checked(not self.checked) - self.hoverEnterEvent() self.clicked.emit() From be1f361b773798e76624900faa37b5da065309f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:56:49 +0200 Subject: [PATCH 051/193] correct component widget is set in main window --- pype/tools/standalonepublish/app.py | 3 +-- .../widgets/widget_component.py | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 2edc345cbb..099566c603 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -46,12 +46,11 @@ class Window(QtWidgets.QDialog): layout_assets.addWidget(widget_assets) layout_assets.addWidget(label_message) - # family widget widget_family = FamilyWidget(self) # components widget - widget_components = DropDataFrame(self) + widget_components = ComponentsWidget(self) # Body body = QtWidgets.QSplitter() diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index 2e65bcf1d8..c068d696ba 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -68,20 +68,20 @@ class ComponentWidget(QtWidgets.QFrame): # Name + representation self.name = QtWidgets.QLabel(frame) - self.frames = QtWidgets.QLabel(frame) + self.file_info = QtWidgets.QLabel(frame) self.ext = QtWidgets.QLabel(frame) self.name.setFont(font) - self.frames.setFont(font) + self.file_info.setFont(font) self.ext.setFont(font) - self.frames.setStyleSheet('padding-left:3px;') + self.file_info.setStyleSheet('padding-left:3px;') expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) frame_name_repre = QtWidgets.QFrame(frame) - self.frames.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) @@ -89,22 +89,31 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.frames, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) frame_name_repre.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding ) - # Frames + icons + # Repre + icons frame_repre_icons = QtWidgets.QFrame(frame) + frame_repre = QtWidgets.QFrame(frame_repre_icons) + label_repre = QtWidgets.QLabel() label_repre.setText('Representation:') self.input_repre = QtWidgets.QLineEdit() self.input_repre.setMaximumWidth(50) + layout = QtWidgets.QHBoxLayout(frame_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + frame_icons = QtWidgets.QFrame(frame_repre_icons) self.preview = SvgButton( @@ -129,8 +138,7 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) frame_middle = QtWidgets.QFrame(frame) From 1d5a74465f5847ffb1930b4342e4ea995be23c80 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:19:57 +0200 Subject: [PATCH 052/193] renamed component to component_item --- .../widgets/widget_component.py | 219 ---------------- .../widgets/widget_component_item.py | 237 ++++++++++++++++-- 2 files changed, 220 insertions(+), 236 deletions(-) delete mode 100644 pype/tools/standalonepublish/widgets/widget_component.py diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py deleted file mode 100644 index c068d696ba..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ /dev/null @@ -1,219 +0,0 @@ -import os -from . import QtCore, QtGui, QtWidgets -from . import SvgButton -from . import get_resource - - -class ComponentWidget(QtWidgets.QFrame): - C_NORMAL = '#777777' - C_HOVER = '#ffffff' - C_ACTIVE = '#4BB543' - C_ACTIVE_HOVER = '#4BF543' - signal_remove = QtCore.Signal(object) - signal_thumbnail = QtCore.Signal(object) - signal_preview = QtCore.Signal(object) - - def __init__(self, parent): - super().__init__() - self.resize(290, 70) - self.setMinimumSize(QtCore.QSize(0, 70)) - self.parent_item = parent - # Font - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - # Main widgets - frame = QtWidgets.QFrame(self) - frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - frame.setFrameShadow(QtWidgets.QFrame.Raised) - - layout_main = QtWidgets.QHBoxLayout(frame) - layout_main.setSpacing(2) - layout_main.setContentsMargins(2, 2, 2, 2) - - # Image + Info - frame_image_info = QtWidgets.QFrame(frame) - - # Layout image info - layout = QtWidgets.QVBoxLayout(frame_image_info) - layout.setSpacing(2) - layout.setContentsMargins(2, 2, 2, 2) - - self.icon = QtWidgets.QLabel(frame) - self.icon.setMinimumSize(QtCore.QSize(22, 22)) - self.icon.setMaximumSize(QtCore.QSize(22, 22)) - self.icon.setText("") - self.icon.setScaledContents(True) - - self.info = SvgButton( - get_resource('information.svg'), 22, 22, - [self.C_NORMAL, self.C_HOVER], - frame_image_info, False - ) - - expanding_sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding - ) - expanding_sizePolicy.setHorizontalStretch(0) - expanding_sizePolicy.setVerticalStretch(0) - - layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) - - layout_main.addWidget(frame_image_info) - - # Name + representation - self.name = QtWidgets.QLabel(frame) - self.file_info = QtWidgets.QLabel(frame) - self.ext = QtWidgets.QLabel(frame) - - self.name.setFont(font) - self.file_info.setFont(font) - self.ext.setFont(font) - - self.file_info.setStyleSheet('padding-left:3px;') - - expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) - - frame_name_repre = QtWidgets.QFrame(frame) - - self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - - layout = QtWidgets.QHBoxLayout(frame_name_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) - - frame_name_repre.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding - ) - - # Repre + icons - frame_repre_icons = QtWidgets.QFrame(frame) - - frame_repre = QtWidgets.QFrame(frame_repre_icons) - - label_repre = QtWidgets.QLabel() - label_repre.setText('Representation:') - - self.input_repre = QtWidgets.QLineEdit() - self.input_repre.setMaximumWidth(50) - - layout = QtWidgets.QHBoxLayout(frame_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) - - frame_icons = QtWidgets.QFrame(frame_repre_icons) - - self.preview = SvgButton( - get_resource('preview.svg'), 64, 18, - [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], - frame_icons - ) - - self.thumbnail = SvgButton( - get_resource('thumbnail.svg'), 84, 18, - [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], - frame_icons - ) - - layout = QtWidgets.QHBoxLayout(frame_icons) - layout.setSpacing(6) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.thumbnail) - layout.addWidget(self.preview) - - layout = QtWidgets.QHBoxLayout(frame_repre_icons) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) - - frame_middle = QtWidgets.QFrame(frame) - - layout = QtWidgets.QVBoxLayout(frame_middle) - layout.setSpacing(0) - layout.setContentsMargins(4, 0, 4, 0) - layout.addWidget(frame_name_repre) - layout.addWidget(frame_repre_icons) - - layout.setStretchFactor(frame_name_repre, 1) - layout.setStretchFactor(frame_repre_icons, 1) - - layout_main.addWidget(frame_middle) - - self.remove = SvgButton( - get_resource('trash.svg'), 22, 22, - [self.C_NORMAL, self.C_HOVER], - frame, False - ) - - layout_main.addWidget(self.remove) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(2, 2, 2, 2) - layout.addWidget(frame) - - self.preview.setToolTip('Mark component as Preview') - self.thumbnail.setToolTip('Component will be selected as thumbnail') - - # self.frame.setStyleSheet("border: 1px solid black;") - - def set_context(self, data): - self.remove.clicked.connect(self._remove) - self.thumbnail.clicked.connect(self._thumbnail_clicked) - self.preview.clicked.connect(self._preview_clicked) - name = data['name'] - representation = data['representation'] - ext = data['ext'] - file_info = data['file_info'] - thumb = data['thumb'] - prev = data['prev'] - info = data['info'] - icon = data['icon'] - - resource = None - if icon is not None: - resource = get_resource('{}.png'.format(icon)) - - if resource is None or not os.path.isfile(resource): - if data['is_sequence']: - resource = get_resource('files.png') - else: - resource = get_resource('file.png') - - pixmap = QtGui.QPixmap(resource) - self.icon.setPixmap(pixmap) - - self.name.setText(name) - self.input_repre.setText(representation) - self.ext.setText('( {} )'.format(ext)) - if file_info is None: - self.file_info.setVisible(False) - else: - self.file_info.setText('[{}]'.format(file_info)) - - self.thumbnail.setVisible(thumb) - self.preview.setVisible(prev) - - def _remove(self): - self.signal_remove.emit(self.parent_item) - - def _thumbnail_clicked(self): - self.signal_thumbnail.emit(self.parent_item) - - def _preview_clicked(self): - self.signal_preview.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index dcf66a9c21..f7221952af 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,29 +1,232 @@ -from . import QtWidgets -from . import ComponentWidget +import os +from . import QtCore, QtGui, QtWidgets +from . import SvgButton +from . import get_resource -class ComponentItem(QtWidgets.QTreeWidgetItem): - def __init__(self, parent, data): - super().__init__(parent) - self.parent_widget = parent +class ComponentItem(QtWidgets.QFrame): + C_NORMAL = '#777777' + C_HOVER = '#ffffff' + C_ACTIVE = '#4BB543' + C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) + signal_thumbnail = QtCore.Signal(object) + signal_preview = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + self.resize(290, 70) + self.setMinimumSize(QtCore.QSize(0, 70)) + self.parent_item = parent + # Font + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + # Main widgets + frame = QtWidgets.QFrame(self) + frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + frame.setFrameShadow(QtWidgets.QFrame.Raised) + + layout_main = QtWidgets.QHBoxLayout(frame) + layout_main.setSpacing(2) + layout_main.setContentsMargins(2, 2, 2, 2) + + # Image + Info + frame_image_info = QtWidgets.QFrame(frame) + + # Layout image info + layout = QtWidgets.QVBoxLayout(frame_image_info) + layout.setSpacing(2) + layout.setContentsMargins(2, 2, 2, 2) + + self.icon = QtWidgets.QLabel(frame) + self.icon.setMinimumSize(QtCore.QSize(22, 22)) + self.icon.setMaximumSize(QtCore.QSize(22, 22)) + self.icon.setText("") + self.icon.setScaledContents(True) + + self.info = SvgButton( + get_resource('information.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame_image_info, False + ) + + expanding_sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) + expanding_sizePolicy.setHorizontalStretch(0) + expanding_sizePolicy.setVerticalStretch(0) + + layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + + layout_main.addWidget(frame_image_info) + + # Name + representation + self.name = QtWidgets.QLabel(frame) + self.file_info = QtWidgets.QLabel(frame) + self.ext = QtWidgets.QLabel(frame) + + self.name.setFont(font) + self.file_info.setFont(font) + self.ext.setFont(font) + + self.file_info.setStyleSheet('padding-left:3px;') + + expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) + + frame_name_repre = QtWidgets.QFrame(frame) + + self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + layout = QtWidgets.QHBoxLayout(frame_name_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) + + frame_name_repre.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Repre + icons + frame_repre_icons = QtWidgets.QFrame(frame) + + frame_repre = QtWidgets.QFrame(frame_repre_icons) + + label_repre = QtWidgets.QLabel() + label_repre.setText('Representation:') + + self.input_repre = QtWidgets.QLineEdit() + self.input_repre.setMaximumWidth(50) + + layout = QtWidgets.QHBoxLayout(frame_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + + frame_icons = QtWidgets.QFrame(frame_repre_icons) + + self.preview = SvgButton( + get_resource('preview.svg'), 64, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + self.thumbnail = SvgButton( + get_resource('thumbnail.svg'), 84, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + layout = QtWidgets.QHBoxLayout(frame_icons) + layout.setSpacing(6) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.thumbnail) + layout.addWidget(self.preview) + + layout = QtWidgets.QHBoxLayout(frame_repre_icons) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) + + frame_middle = QtWidgets.QFrame(frame) + + layout = QtWidgets.QVBoxLayout(frame_middle) + layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.addWidget(frame_name_repre) + layout.addWidget(frame_repre_icons) + + layout.setStretchFactor(frame_name_repre, 1) + layout.setStretchFactor(frame_repre_icons, 1) + + layout_main.addWidget(frame_middle) + + self.remove = SvgButton( + get_resource('trash.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame, False + ) + + layout_main.addWidget(self.remove) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + layout.addWidget(frame) + + self.preview.setToolTip('Mark component as Preview') + self.thumbnail.setToolTip('Component will be selected as thumbnail') + + # self.frame.setStyleSheet("border: 1px solid black;") + + def set_context(self, data): self.in_data = data + self.remove.clicked.connect(self._remove) + self.thumbnail.clicked.connect(self._thumbnail_clicked) + self.preview.clicked.connect(self._preview_clicked) + name = data['name'] + representation = data['representation'] + ext = data['ext'] + file_info = data['file_info'] + thumb = data['thumb'] + prev = data['prev'] + info = data['info'] + icon = data['icon'] - def set_context(self): - self._widget = ComponentWidget(self) - self._widget.set_context(self.in_data) - self.treeWidget().setItemWidget(self, 0, self._widget) + resource = None + if icon is not None: + resource = get_resource('{}.png'.format(icon)) + + if resource is None or not os.path.isfile(resource): + if data['is_sequence']: + resource = get_resource('files.png') + else: + resource = get_resource('file.png') + + pixmap = QtGui.QPixmap(resource) + self.icon.setPixmap(pixmap) + + self.name.setText(name) + self.input_repre.setText(representation) + self.ext.setText('( {} )'.format(ext)) + if file_info is None: + self.file_info.setVisible(False) + else: + self.file_info.setText('[{}]'.format(file_info)) + + self.thumbnail.setVisible(thumb) + self.preview.setVisible(prev) + + def _remove(self): + self.signal_remove.emit(self) + + def _thumbnail_clicked(self): + self.signal_thumbnail.emit(self) + + def _preview_clicked(self): + self.signal_preview.emit(self) def is_thumbnail(self): - return self._widget.thumbnail.checked + return self.thumbnail.checked def change_thumbnail(self, hover=True): - self._widget.thumbnail.change_checked(hover) + self.thumbnail.change_checked(hover) def is_preview(self): - return self._widget.preview.checked + return self.preview.checked def change_preview(self, hover=True): - self._widget.preview.change_checked(hover) - - def double_clicked(*args): - pass + self.preview.change_checked(hover) From 1570800b7bcddad5d3fb78da20035692ca4aa26b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:20:42 +0200 Subject: [PATCH 053/193] tree components replaced by components list --- .../widgets/widget_components_list.py | 90 +++++++++++++++++++ .../widgets/widget_tree_components.py | 14 --- 2 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 pype/tools/standalonepublish/widgets/widget_components_list.py delete mode 100644 pype/tools/standalonepublish/widgets/widget_tree_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components_list.py b/pype/tools/standalonepublish/widgets/widget_components_list.py new file mode 100644 index 0000000000..357bd1e671 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_components_list.py @@ -0,0 +1,90 @@ +from . import QtCore, QtGui, QtWidgets + + +class ComponentsList(QtWidgets.QTableWidget): + def __init__(self, parent=None): + super().__init__(parent=parent) + + self._main_column = 0 + + self.setColumnCount(1) + self.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectRows + ) + self.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + self.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollPerPixel + ) + self.verticalHeader().hide() + + try: + self.verticalHeader().setResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + except Exception: + self.verticalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().hide() + + def count(self): + return self.rowCount() + + def add_widget(self, widget, row=None): + if row is None: + row = self.count() + + self.insertRow(row) + self.setCellWidget(row, self._main_column, widget) + + self.resizeRowToContents(row) + + return row + + def remove_widget(self, row): + self.removeCellWidget(row, self._main_column) + self.removeRow(row) + + def move_widget(self, widget, newRow): + oldRow = self.indexOfWidget(widget) + if oldRow: + self.insertRow(newRow) + # Collect the oldRow after insert to make sure we move the correct + # widget. + oldRow = self.indexOfWidget(widget) + + self.setCellWidget(newRow, self._main_column, widget) + self.resizeRowToContents(oldRow) + + # Remove the old row + self.removeRow(oldRow) + + def clear_widgets(self): + '''Remove all widgets.''' + self.clear() + self.setRowCount(0) + + def widget_index(self, widget): + index = None + for row in range(self.count()): + candidateWidget = self.widget_at(row) + if candidateWidget == widget: + index = row + break + + return index + + def widgets(self): + widgets = [] + for row in range(self.count()): + widget = self.widget_at(row) + widgets.append(widget) + + return widgets + + def widget_at(self, row): + return self.cellWidget(row, self._main_column) diff --git a/pype/tools/standalonepublish/widgets/widget_tree_components.py b/pype/tools/standalonepublish/widgets/widget_tree_components.py deleted file mode 100644 index 76e5a9bce0..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_tree_components.py +++ /dev/null @@ -1,14 +0,0 @@ -from . import QtCore, QtGui, QtWidgets - - -class TreeComponents(QtWidgets.QTreeWidget): - def __init__(self, parent): - super().__init__(parent) - - self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled) - self.setIndentation(28) - self.headerItem().setText(0, 'Components') - - self.setRootIsDecorated(False) - - self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c)) From 3e6f02795c22ad664bbcaf888c1fd7e0affab627 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:11 +0200 Subject: [PATCH 054/193] family widget now use get presets from config --- .../widgets/widget_family.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 4ef7d9bcf8..26dab9bc19 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -4,13 +4,11 @@ import inspect import json from collections import namedtuple -from . import QtWidgets, QtCore, QtGui +from . import QtWidgets, QtCore from . import HelpRole, FamilyRole, ExistsRole, PluginRole from . import FamilyDescriptionWidget -from pype.vendor import six -from avalon import api, io, style -from pype import lib as pypelib +from pypeapp import config class FamilyWidget(QtWidgets.QWidget): @@ -249,14 +247,9 @@ class FamilyWidget(QtWidgets.QWidget): def refresh(self): has_families = False - - path_items = [ - pypelib.get_presets_path(), 'tools', 'standalone_publish.json' - ] - filepath = os.path.sep.join(path_items) - presets = dict() - with open(filepath) as data_file: - presets = json.load(data_file) + presets = config.get_presets().get('tools', {}).get( + 'standalone_publish', {} + ) for creator in presets.get('families', {}).values(): creator = namedtuple("Creator", creator.keys())(*creator.values()) @@ -277,9 +270,6 @@ class FamilyWidget(QtWidgets.QWidget): item.setData(QtCore.Qt.ItemIsEnabled, False) self.list_families.addItem(item) - presets_path = pypelib.get_presets_path() - config_file = os.path.sep.join([presets_path, 'tools', 'creator.json']) - self.list_families.setCurrentItem(self.list_families.item(0)) def echo(self, message): From 0ff2f9efdd3e39a4b53a8fa7451e580b3b314320 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:32 +0200 Subject: [PATCH 055/193] drop files renamed to drop frame --- ...get_drop_files.py => widget_drop_frame.py} | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) rename pype/tools/standalonepublish/widgets/{widget_drop_files.py => widget_drop_frame.py} (84%) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py similarity index 84% rename from pype/tools/standalonepublish/widgets/widget_drop_files.py rename to pype/tools/standalonepublish/widgets/widget_drop_frame.py index fbeddcbb41..e13a7c476c 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_files.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -2,7 +2,7 @@ import os import clique from pypeapp import config from . import QtWidgets, QtCore -from . import ComponentItem, TreeComponents, DropDataWidget +from . import DropEmpty, ComponentsList, ComponentItem class DropDataFrame(QtWidgets.QFrame): @@ -14,10 +14,18 @@ class DropDataFrame(QtWidgets.QFrame): self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) - self.tree_widget = TreeComponents(self) - layout.addWidget(self.tree_widget) + self.components_list = ComponentsList(self) + layout.addWidget(self.components_list) + + self.drop_widget = DropEmpty(self) + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.drop_widget.sizePolicy().hasHeightForWidth()) + self.drop_widget.setSizePolicy(sizePolicy) - self.drop_widget = DropDataWidget(self) layout.addWidget(self.drop_widget) self._refresh_view() @@ -30,18 +38,29 @@ class DropDataFrame(QtWidgets.QFrame): event.accept() def dropEvent(self, event): - paths = self._processMimeData(event.mimeData()) + self.process_ent_mime(event) + event.accept() + + def process_ent_mime(self, ent): + paths = [] + if ent.mimeData().hasUrls(): + paths = self._processMimeData(ent.mimeData()) + else: + # If path is in clipboard as string + try: + path = ent.text() + if os.path.exists(path): + paths.append(path) + else: + print('Dropped invalid file/folder') + except Exception: + pass if paths: self._add_components(paths) - event.accept() def _processMimeData(self, mimeData): paths = [] - if not mimeData.hasUrls(): - print('Dropped invalid file/folder') - return paths - for path in mimeData.urls(): local_path = path.toLocalFile() if os.path.isfile(local_path) or os.path.isdir(local_path): @@ -61,17 +80,16 @@ class DropDataFrame(QtWidgets.QFrame): def _add_item(self, data): # Assign to self so garbage collector wont remove the component # during initialization - self.new_component = ComponentItem(self.tree_widget, data) - self.tree_widget.addTopLevelItem(self.new_component) - self.new_component.set_context() + new_component = ComponentItem(self.components_list) + new_component.set_context(data) + self.components_list.add_widget(new_component) - self.new_component._widget.signal_remove.connect(self._remove_item) - self.new_component._widget.signal_preview.connect(self._set_preview) - self.new_component._widget.signal_thumbnail.connect( + new_component.signal_remove.connect(self._remove_item) + new_component.signal_preview.connect(self._set_preview) + new_component.signal_thumbnail.connect( self._set_thumbnail ) - self.items.append(self.new_component) - self.new_component = None + self.items.append(new_component) self._refresh_view() @@ -100,14 +118,15 @@ class DropDataFrame(QtWidgets.QFrame): in_item.change_preview() def _remove_item(self, item): - root = self.tree_widget.invisibleRootItem() - (item.parent() or root).removeChild(item) - self.items.remove(item) + index = self.components_list.widget_index(item) + self.components_list.remove_widget(index) + if item in self.items: + self.items.remove(item) self._refresh_view() def _refresh_view(self): _bool = len(self.items) == 0 - self.tree_widget.setVisible(not _bool) + self.components_list.setVisible(not _bool) self.drop_widget.setVisible(_bool) self.parent_widget.set_valid_components(not _bool) From da3214fafe32b45101d5194e64e28123017246ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:43 +0200 Subject: [PATCH 056/193] drop data renamed to drop empty --- .../widgets/widget_drop_data.py | 34 ------------ .../widgets/widget_drop_empty.py | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 34 deletions(-) delete mode 100644 pype/tools/standalonepublish/widgets/widget_drop_data.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_empty.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py deleted file mode 100644 index 74e30b4714..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_drop_data.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import logging -import clique -from . import QtWidgets, QtCore, QtGui - - -class DropDataWidget(QtWidgets.QWidget): - - def __init__(self, parent): - '''Initialise DataDropZone widget.''' - super().__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - - CenterAlignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter - - self._label = QtWidgets.QLabel('Drop files here') - layout.addWidget( - self._label, - alignment=CenterAlignment - ) - - def paintEvent(self, event): - super().paintEvent(event) - painter = QtGui.QPainter(self) - pen = QtGui.QPen() - pen.setWidth(1); - pen.setBrush(QtCore.Qt.darkGray); - pen.setStyle(QtCore.Qt.DashLine); - painter.setPen(pen) - painter.drawRect( - 10, 10, - self.rect().width()-15, self.rect().height()-15 - ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_empty.py b/pype/tools/standalonepublish/widgets/widget_drop_empty.py new file mode 100644 index 0000000000..a68b91da59 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_empty.py @@ -0,0 +1,52 @@ +import os +import logging +import clique +from . import QtWidgets, QtCore, QtGui + + +class DropEmpty(QtWidgets.QWidget): + + def __init__(self, parent): + '''Initialise DataDropZone widget.''' + super().__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + + BottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter + TopCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(26) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + self._label = QtWidgets.QLabel('Drag & Drop') + self._label.setFont(font) + self._label.setStyleSheet( + 'background-color: rgb(255, 255, 255, 0);' + ) + + font.setPointSize(12) + self._sub_label = QtWidgets.QLabel('(drop files here)') + self._sub_label.setFont(font) + self._sub_label.setStyleSheet( + 'background-color: rgb(255, 255, 255, 0);' + ) + + layout.addWidget(self._label, alignment=BottomCenterAlignment) + layout.addWidget(self._sub_label, alignment=TopCenterAlignment) + + def paintEvent(self, event): + super().paintEvent(event) + painter = QtGui.QPainter(self) + pen = QtGui.QPen() + pen.setWidth(1); + pen.setBrush(QtCore.Qt.darkGray); + pen.setStyle(QtCore.Qt.DashLine); + painter.setPen(pen) + painter.drawRect( + 10, 10, + self.rect().width()-15, self.rect().height()-15 + ) From 8da831c19336d2e1be3bed96b4696102c6ebe214 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:23:01 +0200 Subject: [PATCH 057/193] publish and browse btns cant be focused and components can handle mime data --- pype/tools/standalonepublish/widgets/__init__.py | 7 +++---- pype/tools/standalonepublish/widgets/button_from_svgs.py | 2 ++ pype/tools/standalonepublish/widgets/widget_components.py | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 21f703d762..9eb18f4d5d 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -22,12 +22,11 @@ from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget -from .widget_drop_data import DropDataWidget -from .widget_component import ComponentWidget -from .widget_tree_components import TreeComponents +from .widget_drop_empty import DropEmpty from .widget_component_item import ComponentItem +from .widget_components_list import ComponentsList -from .widget_drop_files import DropDataFrame +from .widget_drop_frame import DropDataFrame from .widget_components import ComponentsWidget diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py index 3a35bcd838..4255c5f29b 100644 --- a/pype/tools/standalonepublish/widgets/button_from_svgs.py +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -6,6 +6,7 @@ from PyQt5 import QtSvg, QtXml class SvgResizable(QtSvg.QSvgWidget): clicked = QtCore.Signal() + def __init__(self, filepath, width=None, height=None, fill=None): super().__init__() self.xmldoc = minidom.parse(filepath) @@ -13,6 +14,7 @@ class SvgResizable(QtSvg.QSvgWidget): for element in itemlist: if fill: element.setAttribute('fill', str(fill)) + # TODO auto scale if only one is set if width is not None and height is not None: self.setMaximumSize(width, height) self.setMinimumSize(width, height) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 18d4e480d5..9d9f2aeb22 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -14,9 +14,11 @@ class ComponentsWidget(QtWidgets.QWidget): self.btn_browse = QtWidgets.QPushButton('Browse') self.btn_browse.setToolTip('Browse for file(s).') + self.btn_browse.setFocusPolicy(QtCore.Qt.NoFocus) self.btn_publish = QtWidgets.QPushButton('Publish') self.btn_publish.setToolTip('Publishes data.') + self.btn_publish.setFocusPolicy(QtCore.Qt.NoFocus) layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft) layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight) @@ -33,7 +35,6 @@ class ComponentsWidget(QtWidgets.QWidget): layout.addWidget(body) self.btn_browse.clicked.connect(self._browse) - # self.setStyleSheet("border: 1px solid black;") def set_valid(self, in_bool): self.btn_publish.setEnabled(in_bool) @@ -41,6 +42,9 @@ class ComponentsWidget(QtWidgets.QWidget): def set_valid_components(self, in_bool): self.parent_widget.set_valid_components(in_bool) + def process_mime_data(self, mime_data): + self.drop_frame.process_ent_mime(mime_data) + def _browse(self): options = [ QtWidgets.QFileDialog.DontResolveSymlinks, From d39f8b79fad6b0442553f2e1d4b548205d58b879 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:23:51 +0200 Subject: [PATCH 058/193] body has set better stretch factor and app can handle with clipboard --- pype/tools/standalonepublish/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 099566c603..f39f553828 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -64,8 +64,8 @@ class Window(QtWidgets.QDialog): body.addWidget(widget_family) body.addWidget(widget_components) body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) - body.setStretchFactor(body.indexOf(widget_family), 2) - body.setStretchFactor(body.indexOf(widget_components), 3) + body.setStretchFactor(body.indexOf(widget_family), 3) + body.setStretchFactor(body.indexOf(widget_components), 5) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(body) @@ -124,6 +124,12 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(self.NOT_SELECTED) self.widget_family.on_data_changed() + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_V and event.modifiers() == QtCore.Qt.ControlModifier: + clip = QtWidgets.QApplication.clipboard() + self.widget_components.process_mime_data(clip) + super().keyPressEvent(event) + def validation(self): if not self.initialized: return From fd394c64f92e534bc515a91790d1880616589b33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:14:53 +0200 Subject: [PATCH 059/193] data are collected from all parts now --- pype/tools/standalonepublish/app.py | 3 ++- .../standalonepublish/widgets/widget_asset.py | 16 ++++++++++++++-- .../widgets/widget_component_item.py | 10 ++++++++++ .../widgets/widget_components.py | 3 +++ .../widgets/widget_drop_frame.py | 6 ++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index f39f553828..0e8bfaacc1 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -148,7 +148,8 @@ class Window(QtWidgets.QDialog): data = {} data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) - + data.update(self.widget_components.collect_data()) + return data def show(parent=None, debug=False, context=None): diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 82b3700dea..366fb88dfa 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -179,12 +179,24 @@ class AssetWidget(QtWidgets.QWidget): return self.parent_widget.db def collect_data(self): + project = self.db.find_one({'type': 'project'}) + asset = self.db.find_one({'_id': self.get_active_asset()}) data = { - 'project': self.combo_projects.currentText(), - 'asset': get_active_asset + 'project': project, + 'asset': asset, + 'parents': self.get_parents(asset) } return data + def get_parents(self, entity): + output = [] + if entity.get('data', {}).get('visualParent', None) is None: + return output + parent = self.db.find_one({'_id': entity['data']['visualParent']}) + output.append(parent) + output.extend(self.get_parents(parent)) + return output + def _set_projects(self): projects = list() for project in self.db.projects(): diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index f7221952af..9551f4cae6 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -230,3 +230,13 @@ class ComponentItem(QtWidgets.QFrame): def change_preview(self, hover=True): self.preview.change_checked(hover) + + def collect_data(self): + data = { + 'ext': self.in_data['ext'], + 'representation': self.input_repre.text(), + 'files': self.in_data['files'], + 'thumbnail': self.is_thumbnail(), + 'preview': self.is_preview() + } + return data diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 9d9f2aeb22..b17f8d5ebc 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -45,6 +45,9 @@ class ComponentsWidget(QtWidgets.QWidget): def process_mime_data(self, mime_data): self.drop_frame.process_ent_mime(mime_data) + def collect_data(self): + return self.drop_frame.collect_data() + def _browse(self): options = [ QtWidgets.QFileDialog.DontResolveSymlinks, diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index e13a7c476c..9b0d49b744 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -313,3 +313,9 @@ class DropDataFrame(QtWidgets.QFrame): if found is False: self._add_item(data) + + def collect_data(self): + data = {'components' : []} + for item in self.items: + data['components'].append(item.collect_data()) + return data From f02c917a94651c6869436a5cfa82bd1d30e5c841 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:15:22 +0200 Subject: [PATCH 060/193] publish button now prints data to console --- pype/tools/standalonepublish/widgets/widget_components.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index b17f8d5ebc..15222b17d7 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -1,6 +1,7 @@ from . import QtWidgets, QtCore, QtGui from . import DropDataFrame + class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): super().__init__() @@ -35,6 +36,7 @@ class ComponentsWidget(QtWidgets.QWidget): layout.addWidget(body) self.btn_browse.clicked.connect(self._browse) + self.btn_publish.clicked.connect(self._publish) def set_valid(self, in_bool): self.btn_publish.setEnabled(in_bool) @@ -81,3 +83,8 @@ class ComponentsWidget(QtWidgets.QWidget): # process the browsed files/folders for publishing paths = file_dialog.selectedFiles() self.drop_frame._process_paths(paths) + + def _publish(self): + data = self.parent_widget.collect_data() + from pprint import pprint + pprint(data) From 61026da1a42e72192aad36a0a449b07975b9ae66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:17:19 +0200 Subject: [PATCH 061/193] info in component changed to actions --- .../standalonepublish/widgets/widget_component_item.py | 7 +++---- pype/tools/standalonepublish/widgets/widget_drop_frame.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index 9551f4cae6..db35a2c468 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -49,8 +49,8 @@ class ComponentItem(QtWidgets.QFrame): self.icon.setText("") self.icon.setScaledContents(True) - self.info = SvgButton( - get_resource('information.svg'), 22, 22, + self.action_menu = SvgButton( + get_resource('menu.svg'), 22, 22, [self.C_NORMAL, self.C_HOVER], frame_image_info, False ) @@ -62,7 +62,7 @@ class ComponentItem(QtWidgets.QFrame): expanding_sizePolicy.setVerticalStretch(0) layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.action_menu, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -183,7 +183,6 @@ class ComponentItem(QtWidgets.QFrame): file_info = data['file_info'] thumb = data['thumb'] prev = data['prev'] - info = data['info'] icon = data['icon'] resource = None diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index 9b0d49b744..a9c03d8e2e 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -168,7 +168,7 @@ class DropDataFrame(QtWidgets.QFrame): for file in os.listdir(folder_path): if file.startswith(file_base) and file.endswith(file_ext): files.append(os.path.sep.join([folder_path, file])) - info = {} + actions = [] data = { 'files': files, @@ -178,7 +178,7 @@ class DropDataFrame(QtWidgets.QFrame): 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': True, - 'info': info + 'actions': actions } self._process_data(data) @@ -219,7 +219,7 @@ class DropDataFrame(QtWidgets.QFrame): files = [] files.append(remainder) - info = {} + actions = [] data = { 'files': files, @@ -229,7 +229,7 @@ class DropDataFrame(QtWidgets.QFrame): 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': False, - 'info': info + 'actions': actions } self._process_data(data) From 74b11b98ee96b3a269282d0d424c7050dfb4c031 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:18:44 +0200 Subject: [PATCH 062/193] minor changes in component adding --- .../standalonepublish/resources/menu.svg | 12 +++++++ .../widgets/widget_drop_frame.py | 35 +++++++++++++------ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 pype/tools/standalonepublish/resources/menu.svg diff --git a/pype/tools/standalonepublish/resources/menu.svg b/pype/tools/standalonepublish/resources/menu.svg new file mode 100644 index 0000000000..ac1e728011 --- /dev/null +++ b/pype/tools/standalonepublish/resources/menu.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index a9c03d8e2e..b353b23b41 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -67,7 +67,6 @@ class DropDataFrame(QtWidgets.QFrame): paths.append(local_path) else: print('Invalid input: "{}"'.format(local_path)) - return paths def _add_components(self, paths): @@ -158,20 +157,16 @@ class DropDataFrame(QtWidgets.QFrame): def _process_collection(self, collection): file_base = os.path.basename(collection.head) folder_path = os.path.dirname(collection.head) - if file_base[-1] in ['.']: + if file_base[-1] in ['.', '_']: file_base = file_base[:-1] file_ext = collection.tail repr_name = file_ext.replace('.', '') - range = self._get_ranges(collection.indexes) + range = collection.format('{ranges}') - files = [] - for file in os.listdir(folder_path): - if file.startswith(file_base) and file.endswith(file_ext): - files.append(os.path.sep.join([folder_path, file])) actions = [] data = { - 'files': files, + 'files': [file for file in collection], 'name': file_base, 'ext': file_ext, 'file_info': range, @@ -245,7 +240,10 @@ class DropDataFrame(QtWidgets.QFrame): if data['is_sequence']: icon += 's' data['icon'] = icon - data['thumb'] = ext in self.presets['thumbnailable'] + data['thumb'] = ( + ext in self.presets['thumbnailable'] and + data['is_sequence'] is False + ) data['prev'] = ext in self.presets['extensions']['video_file'] found = False @@ -260,9 +258,18 @@ class DropDataFrame(QtWidgets.QFrame): # If both are single files if not new_is_seq and not ex_is_seq: - if data['name'] != item.in_data['name']: + if data['name'] == item.in_data['name']: + found = True + break + paths = data['files'] + paths.extend(item.in_data['files']) + c, r = clique.assemble(paths) + if len(c) == 0: continue + found = True + self._remove_item(item) + self._process_collection(c[0]) break # If new is sequence and ex is single file elif new_is_seq and not ex_is_seq: @@ -279,6 +286,7 @@ class DropDataFrame(QtWidgets.QFrame): paths = data['files'] paths.append(ex_file) collections, remainders = clique.assemble(paths) + self._remove_item(item) self._process_collection(collections[0]) break # If new is single file existing is sequence @@ -288,6 +296,13 @@ class DropDataFrame(QtWidgets.QFrame): new_file = data['files'][0] found = True if new_file in item.in_data['files']: + paths = [] + for path in item.in_data['files']: + if os.path.exists(path): + paths.append(path) + if len(paths) == 1: + self._remove_item(item) + found = False break paths = item.in_data['files'] paths.append(new_file) From 2939984216b1eba3f43042a5f4cb83d24b7ed110 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 18 Apr 2019 13:57:41 +0200 Subject: [PATCH 063/193] feat(nuke): adding `update` function for integrate manager --- pype/plugins/nuke/load/load_sequence.py | 83 +++++++++++++++++-------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 45cd6e616e..577a499954 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -88,8 +88,6 @@ class LoadSequence(api.Loader): containerise, viewer_update_and_undo_stop ) - # for k, v in context.items(): - # log.info("key: `{}`, value: {}\n".format(k, v)) version = context['version'] version_data = version.get("data", {}) @@ -137,12 +135,14 @@ class LoadSequence(api.Loader): data_imprint.update({k: context["version"]['data'][k]}) data_imprint.update({"objectName": read_name}) + r["tile_color"].setValue(int("0x4ecd25ff", 16)) + return containerise(r, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) def switch(self, container, representation): self.update(container, representation) @@ -150,18 +150,17 @@ class LoadSequence(api.Loader): def update(self, container, representation): """Update the Loader's path - Fusion automatically tries to reset some variables when changing + Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ from avalon.nuke import ( - viewer_update_and_undo_stop, ls_img_sequence, update_container ) - log.info("this i can see") + node = nuke.toNode(container['objectName']) # TODO: prepare also for other Read img/geo/camera assert node.Class() == "Read", "Must be Read" @@ -170,8 +169,19 @@ class LoadSequence(api.Loader): file = ls_img_sequence(os.path.dirname(root), one=True) # Get start frame from version data - version = io.find_one({"type": "version", - "_id": representation["parent"]}) + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + start = version["data"].get("startFrame") if start is None: log.warning("Missing start frame for updated version" @@ -179,24 +189,43 @@ class LoadSequence(api.Loader): "{} ({})".format(node['name'].value(), representation)) start = 0 - with viewer_update_and_undo_stop(): + # Update the loader's path whilst preserving some values + with preserve_trim(node): + node["file"].setValue(file["path"]) + log.info("__ node['file']: {}".format(node["file"])) - # Update the loader's path whilst preserving some values - with preserve_trim(node): - node["file"].setValue(file["path"]) + # Set the global in to the start frame of the sequence + global_in_changed = loader_shift(node, start, relative=False) + if global_in_changed: + # Log this change to the user + log.debug("Changed '{}' global in:" + " {:d}".format(node['name'].value(), start)) - # Set the global in to the start frame of the sequence - global_in_changed = loader_shift(node, start, relative=False) - if global_in_changed: - # Log this change to the user - log.debug("Changed '{}' global in:" - " {:d}".format(node['name'].value(), start)) + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "startFrame": start, + "endFrame": version["data"].get("endFrame"), + "version": version.get("name"), + "colorspace": version["data"].get("colorspace"), + "source": version["data"].get("source"), + "handles": version["data"].get("handles"), + "fps": version["data"].get("fps"), + "author": version["data"].get("author"), + "outputDir": version["data"].get("outputDir"), + }) - # Update the imprinted representation - update_container( - node, - {"representation": str(representation["_id"])} - ) + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) def remove(self, container): From 550243f0004c0eeedffcaab3a8c3af73940bebcf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:21:15 +0200 Subject: [PATCH 064/193] fix(nuke): adding hierarchical attributes to check and ignore --- pype/plugins/nuke/publish/validate_script.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index 5939083a61..ad4a83b32f 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -28,7 +28,7 @@ class ValidateScript(pyblish.api.InstancePlugin): ] # Value of these attributes can be found on parents - hierarchical_attributes = ["fps"] + hierarchical_attributes = ["fps", "resolution_width", "resolution_height", "pixel_aspect"] missing_attributes = [] asset_attributes = {} @@ -80,6 +80,7 @@ class ValidateScript(pyblish.api.InstancePlugin): # Compare asset's values Nukescript X Database not_matching = [] for attr in attributes: + self.log.debug("asset vs script attribute: {0}, {1}".format(asset_attributes[attr], script_attributes[attr])) if asset_attributes[attr] != script_attributes[attr]: not_matching.append(attr) From fb05104150725fba71424e29d79ab2b5345cf3bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:20:08 +0200 Subject: [PATCH 065/193] feat(nuke): dealing with collection if none for nonsequencial files --- pype/plugins/nuke/publish/extract_review.py | 68 +++++++++++++-------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/pype/plugins/nuke/publish/extract_review.py b/pype/plugins/nuke/publish/extract_review.py index e85185e919..16fb07a3fc 100644 --- a/pype/plugins/nuke/publish/extract_review.py +++ b/pype/plugins/nuke/publish/extract_review.py @@ -28,21 +28,29 @@ class ExtractDataForReview(pype.api.Extractor): self.log.debug("creating staging dir:") self.staging_dir(instance) - self.render_review_representation(instance, - representation="mov") - self.log.debug("review mov:") - self.transcode_mov(instance) - self.render_review_representation(instance, - representation="jpeg") + self.log.debug("instance: {}".format(instance)) + self.log.debug("instance.data[families]: {}".format( + instance.data["families"])) + + if "still" not in instance.data["families"]: + self.render_review_representation(instance, + representation="mov") + self.log.debug("review mov:") + self.transcode_mov(instance) + self.log.debug("instance.data: {}".format(instance.data)) + self.render_review_representation(instance, + representation="jpeg") + else: + self.log.debug("instance: {}".format(instance)) + self.render_review_representation(instance, representation="jpeg") + # Restore selection [i["selected"].setValue(False) for i in nuke.allNodes()] [i["selected"].setValue(True) for i in selection] def transcode_mov(self, instance): - import subprocess - collection = instance.data["collection"] - staging_dir = instance.data["stagingDir"] + staging_dir = instance.data["stagingDir"].replace("\\", "/") file_name = collection.format("{head}mov") review_mov = os.path.join(staging_dir, file_name).replace("\\", "/") @@ -53,13 +61,16 @@ class ExtractDataForReview(pype.api.Extractor): out, err = ( ffmpeg .input(input_movie) - .output(review_mov, pix_fmt='yuv420p', crf=18, timecode="00:00:00:01") + .output( + review_mov, + pix_fmt='yuv420p', + crf=18, + timecode="00:00:00:01" + ) .overwrite_output() .run() ) - - self.log.debug("Removing `{0}`...".format( instance.data["baked_colorspace_movie"])) os.remove(instance.data["baked_colorspace_movie"]) @@ -72,23 +83,32 @@ class ExtractDataForReview(pype.api.Extractor): assert instance.data['files'], "Instance data files should't be empty!" - import clique import nuke temporary_nodes = [] - staging_dir = instance.data["stagingDir"] + staging_dir = instance.data["stagingDir"].replace("\\", "/") + self.log.debug("StagingDir `{0}`...".format(staging_dir)) collection = instance.data.get("collection", None) - # Create nodes - first_frame = min(collection.indexes) - last_frame = max(collection.indexes) + if collection: + # get path + fname = os.path.basename(collection.format( + "{head}{padding}{tail}")) + fhead = collection.format("{head}") + + # get first and last frame + first_frame = min(collection.indexes) + last_frame = max(collection.indexes) + else: + fname = os.path.basename(instance.data.get("path", None)) + fhead = os.path.splitext(fname)[0] + "." + first_frame = instance.data.get("startFrame", None) + last_frame = instance.data.get("endFrame", None) node = previous_node = nuke.createNode("Read") node["file"].setValue( - os.path.join(staging_dir, - os.path.basename(collection.format( - "{head}{padding}{tail}"))).replace("\\", "/")) + os.path.join(staging_dir, fname).replace("\\", "/")) node["first"].setValue(first_frame) node["origfirst"].setValue(first_frame) @@ -126,7 +146,7 @@ class ExtractDataForReview(pype.api.Extractor): write_node = nuke.createNode("Write") if representation in "mov": - file = collection.format("{head}baked.mov") + file = fhead + "baked.mov" path = os.path.join(staging_dir, file).replace("\\", "/") self.log.debug("Path: {}".format(path)) instance.data["baked_colorspace_movie"] = path @@ -137,7 +157,7 @@ class ExtractDataForReview(pype.api.Extractor): temporary_nodes.append(write_node) elif representation in "jpeg": - file = collection.format("{head}jpeg") + file = fhead + "jpeg" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) @@ -147,8 +167,8 @@ class ExtractDataForReview(pype.api.Extractor): temporary_nodes.append(write_node) # retime for - first_frame = int(last_frame)/2 - last_frame = int(last_frame)/2 + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 # add into files for integration as representation instance.data["files"].append(file) From 5994b19f07d5ae729a2f99113fa57321674894db Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:18:58 +0200 Subject: [PATCH 066/193] feat(nuke): dealing with collection if none for nonsequential files --- pype/plugins/nuke/publish/extract_render_local.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pype/plugins/nuke/publish/extract_render_local.py b/pype/plugins/nuke/publish/extract_render_local.py index 1f0a00273f..5ac1c77059 100644 --- a/pype/plugins/nuke/publish/extract_render_local.py +++ b/pype/plugins/nuke/publish/extract_render_local.py @@ -17,18 +17,11 @@ class NukeRenderLocal(pype.api.Extractor): order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] - families = ["render.local", "prerender.local", "still.local"] + families = ["render.local"] def process(self, instance): node = instance[0] - # This should be a ContextPlugin, but this is a workaround - # for a bug in pyblish to run once for a family: issue #250 context = instance.context - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True self.log.debug("instance collected: {}".format(instance.data)) @@ -70,8 +63,9 @@ class NukeRenderLocal(pype.api.Extractor): collections, remainder = clique.assemble(*instance.data['files']) self.log.info('collections: {}'.format(str(collections))) - collection = collections[0] - instance.data['collection'] = collection + if collections: + collection = collections[0] + instance.data['collection'] = collection self.log.info('Finished render') return From 451eb8df55d70d33c2e890f3cf4480692e6ba152 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:17:39 +0200 Subject: [PATCH 067/193] fix(nuke): cleaning comments and beautifying --- pype/plugins/nuke/publish/collect_writes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 612c2a8775..59434e3bec 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,5 +1,4 @@ import os -import tempfile import nuke import pyblish.api import logging @@ -65,9 +64,6 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): int(last_frame) ) - # preredered frames - # collect frames by try - # collect families in next file if "files" not in instance.data: instance.data["files"] = list() try: @@ -89,8 +85,6 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): "colorspace": node["colorspace"].value(), }) - - self.log.debug("instance.data: {}".format(instance.data)) self.log.debug("context: {}".format(context)) From 1fc9dbe56fa44e3c5f6cd6eb597ccdbff0390382 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:15:04 +0200 Subject: [PATCH 068/193] fix(nuke): families add inserting way instead of applending --- pype/plugins/nuke/publish/collect_families.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_families.py b/pype/plugins/nuke/publish/collect_families.py index 08ab90143d..d7515f91ca 100644 --- a/pype/plugins/nuke/publish/collect_families.py +++ b/pype/plugins/nuke/publish/collect_families.py @@ -18,7 +18,7 @@ class CollectInstanceFamilies(pyblish.api.InstancePlugin): families = [] if instance.data.get('families'): - families.append(instance.data['families']) + families += instance.data['families'] # set for ftrack to accept # instance.data["families"] = ["ftrack"] @@ -36,10 +36,8 @@ class CollectInstanceFamilies(pyblish.api.InstancePlugin): families.append('ftrack') - instance.data["families"] = families - # Sort/grouped by family (preserving local index) instance.context[:] = sorted(instance.context, key=self.sort_by_family) From 1bbe00744cc4d090d3f917cbce10551dd5b39795 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:12:32 +0200 Subject: [PATCH 069/193] fix(nuke): disabling create.CreateStillWrite --- pype/plugins/nuke/create/create_write.py | 95 ++++++++++++------------ 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py index 2c9ff42f98..b3c9117641 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write.py @@ -35,7 +35,7 @@ class CrateWriteRender(avalon.nuke.Creator): data = OrderedDict() - data["family"] = self.family + data["family"] = self.family.split("_")[-1] data["families"] = self.families {data.update({k: v}) for k, v in self.data.items() @@ -103,48 +103,51 @@ class CrateWritePrerender(avalon.nuke.Creator): create_write_node(self.data["subset"], write_data) return - - -class CrateWriteStill(avalon.nuke.Creator): - # change this to template preset - preset = "still" - - name = "WriteStill" - label = "Create Write Still" - hosts = ["nuke"] - family = "{}_write".format(preset) - families = preset - icon = "image" - - def __init__(self, *args, **kwargs): - super(CrateWriteStill, self).__init__(*args, **kwargs) - - data = OrderedDict() - - data["family"] = self.family - data["families"] = self.families - - {data.update({k: v}) for k, v in self.data.items() - if k not in data.keys()} - self.data = data - - def process(self): - self.name = self.data["subset"] - - instance = nuke.toNode(self.data["subset"]) - - family = self.family - node = 'write' - - if not instance: - write_data = { - "frame_range": [nuke.frame(), nuke.frame()], - "class": node, - "preset": self.preset, - "avalon": self.data - } - - nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) - create_write_node(self.data["subset"], write_data) - - return +# +# +# class CrateWriteStill(avalon.nuke.Creator): +# # change this to template preset +# preset = "still" +# +# name = "WriteStill" +# label = "Create Write Still" +# hosts = ["nuke"] +# family = "{}_write".format(preset) +# families = preset +# icon = "image" +# +# def __init__(self, *args, **kwargs): +# super(CrateWriteStill, self).__init__(*args, **kwargs) +# +# data = OrderedDict() +# +# data["family"] = self.family.split("_")[-1] +# data["families"] = self.families +# +# {data.update({k: v}) for k, v in self.data.items() +# if k not in data.keys()} +# self.data = data +# +# def process(self): +# self.name = self.data["subset"] +# +# node_name = self.data["subset"].replace( +# "_", "_f{}_".format(nuke.frame())) +# instance = nuke.toNode(self.data["subset"]) +# self.data["subset"] = node_name +# +# family = self.family +# node = 'write' +# +# if not instance: +# write_data = { +# "frame_range": [nuke.frame(), nuke.frame()], +# "class": node, +# "preset": self.preset, +# "avalon": self.data +# } +# +# nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) +# create_write_node(node_name, write_data) +# +# return From 14e76f64250f42d75cefd2958d27b59d48d31abf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Apr 2019 18:11:27 +0200 Subject: [PATCH 070/193] fix(aport): cleanup pype.aport dir --- pype/aport/{ => original}/pipeline.py | 0 pype/aport/{ => original}/templates.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pype/aport/{ => original}/pipeline.py (100%) rename pype/aport/{ => original}/templates.py (100%) diff --git a/pype/aport/pipeline.py b/pype/aport/original/pipeline.py similarity index 100% rename from pype/aport/pipeline.py rename to pype/aport/original/pipeline.py diff --git a/pype/aport/templates.py b/pype/aport/original/templates.py similarity index 100% rename from pype/aport/templates.py rename to pype/aport/original/templates.py From 3a0ebe44fb5b56b2366e35dc550eb19d11422ef5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:36:13 +0200 Subject: [PATCH 071/193] created shadow widget can be used when app is working --- .../widgets/widget_shadow.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_shadow.py diff --git a/pype/tools/standalonepublish/widgets/widget_shadow.py b/pype/tools/standalonepublish/widgets/widget_shadow.py new file mode 100644 index 0000000000..1bb9cee44b --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_shadow.py @@ -0,0 +1,40 @@ +from . import QtWidgets, QtCore, QtGui + + +class ShadowWidget(QtWidgets.QWidget): + def __init__(self, parent): + self.parent_widget = parent + super().__init__(parent) + w = self.parent_widget.frameGeometry().width() + h = self.parent_widget.frameGeometry().height() + self.resize(QtCore.QSize(w, h)) + palette = QtGui.QPalette(self.palette()) + palette.setColor(palette.Background, QtCore.Qt.transparent) + self.setPalette(palette) + self.message = '' + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(40) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + self.font = font + + def paintEvent(self, event): + painter = QtGui.QPainter() + painter.begin(self) + painter.setFont(self.font) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.fillRect(event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))) + painter.drawText( + QtCore.QRectF( + 0.0, + 0.0, + self.parent_widget.frameGeometry().width(), + self.parent_widget.frameGeometry().height() + ), + QtCore.Qt.AlignCenter|QtCore.Qt.AlignCenter, + self.message + ) + painter.end() From 3781f1cec584c0703671bdaf71f0e864b5a09de0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:36:55 +0200 Subject: [PATCH 072/193] implemented shadow widget into main app --- pype/tools/standalonepublish/app.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 0e8bfaacc1..f45c8bd0cd 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -94,6 +94,18 @@ class Window(QtWidgets.QDialog): # Refresh asset input in Family widget self.on_asset_changed() self.validation() + self.shadow_widget = ShadowWidget(self) + self.shadow_widget.setVisible(False) + + def resizeEvent(self, event=None): + position_x = (self.frameGeometry().width()-self.shadow_widget.frameGeometry().width())/2 + position_y = (self.frameGeometry().height()-self.shadow_widget.frameGeometry().height())/2 + self.shadow_widget.move(position_x, position_y) + w = self.frameGeometry().width() + h = self.frameGeometry().height() + self.shadow_widget.resize(QtCore.QSize(w, h)) + if event: + super().resizeEvent(event) def get_avalon_parent(self, entity): parent_id = entity['data']['visualParent'] @@ -130,6 +142,17 @@ class Window(QtWidgets.QDialog): self.widget_components.process_mime_data(clip) super().keyPressEvent(event) + def working_start(self, msg=None): + if msg is None: + msg = 'Please wait...' + self.shadow_widget.message = msg + self.shadow_widget.setVisible(True) + self.resizeEvent() + QtWidgets.QApplication.processEvents() + + def working_stop(self): + self.shadow_widget.setVisible(False) + def validation(self): if not self.initialized: return From 0019004bc285d9a4c9526162ba7bbdf1c9ca2336 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:42:01 +0200 Subject: [PATCH 073/193] added publish that can launch pyblish --- pype/tools/standalonepublish/publish.py | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 pype/tools/standalonepublish/publish.py diff --git a/pype/tools/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py new file mode 100644 index 0000000000..3bc225019f --- /dev/null +++ b/pype/tools/standalonepublish/publish.py @@ -0,0 +1,77 @@ +import os +import sys +import json +import tempfile +import random +import string + +from avalon import io +from avalon import api as avalon + +import pype +from pypeapp import execute + +import pyblish.api + + +pype.install() + + +def set_context(project, asset, app): + os.environ["AVALON_PROJECT"] = project + os.environ["AVALON_ASSET"] = asset + + io.install() + + av_project = io.find_one({'type': 'project'}) + av_asset = io.find_one({ + "type": 'asset', + "name": asset + }) + + parents = av_asset['data']['parents'] + hierarchy = '' + if parents and len(parents) > 0: + hierarchy = os.path.sep.join(parents) + + os.environ["AVALON_HIEARCHY"] = hierarchy + io.Session["AVALON_HIEARCHY"] = hierarchy + + os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + + io.Session["current_dir"] = os.path.normpath(os.getcwd()) + + os.environ["AVALON_APP"] = app + io.Session["AVALON_APP"] = app + + io.uninstall() + + +def publish(data, gui=True): + io.install() + + # Create hash name folder in temp + chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) + staging_dir = tempfile.mkdtemp(chars)#.replace("\\", "/") + + # create also json and fill with data + json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' + with open(json_data_path, 'w') as outfile: + json.dump(data, outfile) + + args = [ + "-pp", os.pathsep.join(pyblish.api.registered_paths()) + ] + + if gui: + args += ["gui"] + + os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["ASAPUBLISH_INPATH"] = json_data_path + + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) + + io.uninstall() From 0a31e71bf4e5463b77fe48a86a5c101c976b5699 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:45:25 +0200 Subject: [PATCH 074/193] added split and merge actions into components --- .../standalonepublish/widgets/__init__.py | 2 + .../widgets/widget_component_item.py | 48 ++++++- .../widgets/widget_components.py | 8 ++ .../widgets/widget_drop_frame.py | 121 ++++++++++-------- 4 files changed, 119 insertions(+), 60 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 9eb18f4d5d..cd99e15bed 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -30,3 +30,5 @@ from .widget_components_list import ComponentsList from .widget_drop_frame import DropDataFrame from .widget_components import ComponentsWidget + +from.widget_shadow import ShadowWidget diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index db35a2c468..6dec892d91 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -2,6 +2,7 @@ import os from . import QtCore, QtGui, QtWidgets from . import SvgButton from . import get_resource +from avalon import style class ComponentItem(QtWidgets.QFrame): @@ -13,11 +14,13 @@ class ComponentItem(QtWidgets.QFrame): signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) - def __init__(self, parent): + def __init__(self, parent, main_parent): super().__init__() + self.actions = [] self.resize(290, 70) self.setMinimumSize(QtCore.QSize(0, 70)) - self.parent_item = parent + self.parent_list = parent + self.parent_widget = main_parent # Font font = QtGui.QFont() font.setFamily("DejaVu Sans Condensed") @@ -49,12 +52,14 @@ class ComponentItem(QtWidgets.QFrame): self.icon.setText("") self.icon.setScaledContents(True) - self.action_menu = SvgButton( + self.btn_action_menu = SvgButton( get_resource('menu.svg'), 22, 22, [self.C_NORMAL, self.C_HOVER], frame_image_info, False ) + self.action_menu = QtWidgets.QMenu() + expanding_sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) @@ -62,7 +67,7 @@ class ComponentItem(QtWidgets.QFrame): expanding_sizePolicy.setVerticalStretch(0) layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.action_menu, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.btn_action_menu, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -173,6 +178,7 @@ class ComponentItem(QtWidgets.QFrame): # self.frame.setStyleSheet("border: 1px solid black;") def set_context(self, data): + self.btn_action_menu.setVisible(False) self.in_data = data self.remove.clicked.connect(self._remove) self.thumbnail.clicked.connect(self._thumbnail_clicked) @@ -209,6 +215,39 @@ class ComponentItem(QtWidgets.QFrame): self.thumbnail.setVisible(thumb) self.preview.setVisible(prev) + def add_action(self, action_name): + if action_name.lower() == 'split': + for action in self.actions: + if action.text() == 'Split to frames': + return + new_action = QtWidgets.QAction('Split to frames', self) + new_action.triggered.connect(self.split_sequence) + elif action_name.lower() == 'merge': + for action in self.actions: + if action.text() == 'Merge components': + return + new_action = QtWidgets.QAction('Merge components', self) + new_action.triggered.connect(self.merge_sequence) + else: + print('unknown action') + return + self.action_menu.addAction(new_action) + self.actions.append(new_action) + if not self.btn_action_menu.isVisible(): + self.btn_action_menu.setVisible(True) + self.btn_action_menu.clicked.connect(self.show_actions) + self.action_menu.setStyleSheet(style.load_stylesheet()) + + def split_sequence(self): + self.parent_widget.split_items(self) + + def merge_sequence(self): + self.parent_widget.merge_items(self) + + def show_actions(self): + position = QtGui.QCursor().pos() + self.action_menu.popup(position) + def _remove(self): self.signal_remove.emit(self) @@ -233,6 +272,7 @@ class ComponentItem(QtWidgets.QFrame): def collect_data(self): data = { 'ext': self.in_data['ext'], + 'label': self.name.text(), 'representation': self.input_repre.text(), 'files': self.in_data['files'], 'thumbnail': self.is_thumbnail(), diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 15222b17d7..61465f2ce5 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -84,6 +84,14 @@ class ComponentsWidget(QtWidgets.QWidget): paths = file_dialog.selectedFiles() self.drop_frame._process_paths(paths) + def working_start(self, msg=None): + if hasattr(self, 'parent_widget'): + self.parent_widget.working_start(msg) + + def working_stop(self): + if hasattr(self, 'parent_widget'): + self.parent_widget.working_stop() + def _publish(self): data = self.parent_widget.collect_data() from pprint import pprint diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index b353b23b41..ec8b07ce34 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -56,7 +56,7 @@ class DropDataFrame(QtWidgets.QFrame): except Exception: pass if paths: - self._add_components(paths) + self._process_paths(paths) def _processMimeData(self, mimeData): paths = [] @@ -69,17 +69,10 @@ class DropDataFrame(QtWidgets.QFrame): print('Invalid input: "{}"'.format(local_path)) return paths - def _add_components(self, paths): - components = self._process_paths(paths) - if not components: - return - for component in components: - self._add_item(component) - - def _add_item(self, data): + def _add_item(self, data, actions=[]): # Assign to self so garbage collector wont remove the component # during initialization - new_component = ComponentItem(self.components_list) + new_component = ComponentItem(self.components_list, self) new_component.set_context(data) self.components_list.add_widget(new_component) @@ -88,6 +81,9 @@ class DropDataFrame(QtWidgets.QFrame): new_component.signal_thumbnail.connect( self._set_thumbnail ) + for action in actions: + new_component.add_action(action) + self.items.append(new_component) self._refresh_view() @@ -131,12 +127,14 @@ class DropDataFrame(QtWidgets.QFrame): self.parent_widget.set_valid_components(not _bool) def _process_paths(self, in_paths): + self.parent_widget.working_start() paths = self._get_all_paths(in_paths) collections, remainders = clique.assemble(paths) for collection in collections: self._process_collection(collection) for remainder in remainders: self._process_remainder(remainder) + self.parent_widget.working_stop() def _get_all_paths(self, paths): output_paths = [] @@ -246,6 +244,9 @@ class DropDataFrame(QtWidgets.QFrame): ) data['prev'] = ext in self.presets['extensions']['video_file'] + actions = [] + new_is_seq = data['is_sequence'] + found = False for item in self.items: if data['ext'] != item.in_data['ext']: @@ -253,7 +254,6 @@ class DropDataFrame(QtWidgets.QFrame): if data['folder_path'] != item.in_data['folder_path']: continue - new_is_seq = data['is_sequence'] ex_is_seq = item.in_data['is_sequence'] # If both are single files @@ -266,68 +266,77 @@ class DropDataFrame(QtWidgets.QFrame): c, r = clique.assemble(paths) if len(c) == 0: continue + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) - found = True - self._remove_item(item) - self._process_collection(c[0]) - break # If new is sequence and ex is single file elif new_is_seq and not ex_is_seq: if data['name'] not in item.in_data['name']: continue ex_file = item.in_data['files'][0] - found = True - # If file is one of inserted sequence - if ex_file in data['files']: - self._remove_item(item) - self._add_item(data) - break - # if file is missing in inserted sequence - paths = data['files'] - paths.append(ex_file) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) - break + + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) + continue + # If new is single file existing is sequence elif not new_is_seq and ex_is_seq: if item.in_data['name'] not in data['name']: continue - new_file = data['files'][0] - found = True - if new_file in item.in_data['files']: - paths = [] - for path in item.in_data['files']: - if os.path.exists(path): - paths.append(path) - if len(paths) == 1: - self._remove_item(item) - found = False - break - paths = item.in_data['files'] - paths.append(new_file) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) - break # If both are sequence else: if data['name'] != item.in_data['name']: continue - found = True - ex_files = item.in_data['files'] - for file in data['files']: - if file not in ex_files: - ex_files.append(file) - paths = list(set(ex_files)) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) - break + if data['files'] == item.in_data['files']: + found = True + break + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) + + if new_is_seq: + actions.append('split') if found is False: - self._add_item(data) + self._add_item(data, actions) + + def merge_items(self, in_item): + self.parent_widget.working_start() + items = [] + in_paths = in_item.in_data['files'] + paths = in_paths + for item in self.items: + if item.in_data['files'] == in_paths: + items.append(item) + continue + copy_paths = paths.copy() + copy_paths.extend(item.in_data['files']) + collections, remainders = clique.assemble(copy_paths) + if len(collections) == 1 and len(remainders) == 0: + paths.extend(item.in_data['files']) + items.append(item) + for item in items: + self._remove_item(item) + self._process_paths(paths) + self.parent_widget.working_stop() + + def split_items(self, item): + self.parent_widget.working_start() + paths = item.in_data['files'] + self._remove_item(item) + for path in paths: + self._process_remainder(path) + self.parent_widget.working_stop() def collect_data(self): data = {'components' : []} From 91b170f0ac205b3db10da0b25e7f82c3f0dee19f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:47:39 +0200 Subject: [PATCH 075/193] added basic file info getter (ffprobe must be in PATH) --- .../widgets/widget_drop_frame.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index ec8b07ce34..1c9c9ea359 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -1,5 +1,6 @@ import os import clique +import subprocess from pypeapp import config from . import QtWidgets, QtCore from . import DropEmpty, ComponentsList, ComponentItem @@ -218,15 +219,42 @@ class DropDataFrame(QtWidgets.QFrame): 'files': files, 'name': file_base, 'ext': file_ext, - 'file_info': file_info, 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': False, 'actions': actions } + data['file_info'] = self.get_file_info(data) self._process_data(data) + def get_file_info(self, data): + output = None + if data['ext'] == '.mov': + try: + # ffProbe must be in PATH + filepath = data['files'][0] + args = ['ffprobe', '-show_streams', filepath] + p = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + datalines=[] + for line in iter(p.stdout.readline, b''): + line = line.decode("utf-8").replace('\r\n', '') + datalines.append(line) + + find_value = 'codec_name' + for line in datalines: + if line.startswith(find_value): + output = line.replace(find_value + '=', '') + break + except Exception as e: + pass + return output + def _process_data(self, data): ext = data['ext'] icon = 'default' From adc063d3ff52702e33b3eeebda280185c95201af Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:48:06 +0200 Subject: [PATCH 076/193] publish button opens publish GUI --- .../standalonepublish/widgets/widget_components.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 61465f2ce5..5cc66de0b5 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -1,6 +1,8 @@ from . import QtWidgets, QtCore, QtGui from . import DropDataFrame +from .. import publish + class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): @@ -93,6 +95,12 @@ class ComponentsWidget(QtWidgets.QWidget): self.parent_widget.working_stop() def _publish(self): - data = self.parent_widget.collect_data() - from pprint import pprint - pprint(data) + self.working_start('Pyblish is running') + try: + data = self.parent_widget.collect_data() + publish.set_context( + data['project'], data['asset'], 'standalonepublish' + ) + publish.publish(data) + finally: + self.working_stop() From 96684f42730442727c178a1e9d997e55b150ebf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:50:34 +0200 Subject: [PATCH 077/193] assets are collecting json serializable data --- pype/tools/standalonepublish/widgets/widget_asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 366fb88dfa..45e9757d71 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -182,8 +182,8 @@ class AssetWidget(QtWidgets.QWidget): project = self.db.find_one({'type': 'project'}) asset = self.db.find_one({'_id': self.get_active_asset()}) data = { - 'project': project, - 'asset': asset, + 'project': project['name'], + 'asset': asset['name'], 'parents': self.get_parents(asset) } return data @@ -193,7 +193,7 @@ class AssetWidget(QtWidgets.QWidget): if entity.get('data', {}).get('visualParent', None) is None: return output parent = self.db.find_one({'_id': entity['data']['visualParent']}) - output.append(parent) + output.append(parent['name']) output.extend(self.get_parents(parent)) return output From 7128389411e189f99acd14db2b7a906eda30901f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:51:33 +0200 Subject: [PATCH 078/193] created first standalone publish collector MUST EDIT --- .../publish/collect_context.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 pype/plugins/standalonepublish/publish/collect_context.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py new file mode 100644 index 0000000000..d063bcf2dd --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -0,0 +1,73 @@ +import os +import pyblish.api +from avalon import ( + io, + api as avalon +) +import json +import logging + + +log = logging.getLogger("collector") + + +class CollectContextDataSAPublish(pyblish.api.ContextPlugin): + """ + Collecting temp json data sent from a host context + and path for returning json data back to hostself. + + Setting avalon session into correct context + + Args: + context (obj): pyblish context session + + """ + + label = "Collect Context - SA Publish" + order = pyblish.api.CollectorOrder - 0.49 + + def process(self, context): + # get json paths from os and load them + io.install() + json_path = os.environ.get("ASAPUBLISH_INPATH") + with open(json_path, "r") as f: + in_data = json.load(f) + + context.data["stagingDir"] = os.path.dirname(json_path) + project_name = in_data['project'] + asset_name = in_data['asset'] + family = in_data['family'] + subset = in_data['subset'] + + project = io.find_one({'type': 'project'}) + asset = io.find_one({ + 'type': 'asset', + 'name': asset_name + }) + context.data['project'] = project + context.data['asset'] = asset + context.data['family'] = family + context.data['subset'] = subset + + instances = [] + + for component in in_data['components']: + instance = context.create_instance(subset) + # instance.add(node) + + instance.data.update({ + "subset": subset, + "asset": asset_name, + "label": component['label'], + "name": component['representation'], + "subset": subset, + "family": family, + "is_thumbnail": component['thumbnail'], + "is_preview": component['preview'] + }) + + self.log.info("collected instance: {}".format(instance.data)) + instances.append(instance) + + context.data["instances"] = instances + self.log.info(in_data) From 764fd0493f66ad769c39c4215f50b85ab2d8f3c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:55:49 +0200 Subject: [PATCH 079/193] added register of standalone publish plugins path --- pype/tools/standalonepublish/publish.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py index 3bc225019f..bb6c7215b4 100644 --- a/pype/tools/standalonepublish/publish.py +++ b/pype/tools/standalonepublish/publish.py @@ -15,6 +15,10 @@ import pyblish.api pype.install() +PUBLISH_PATH = os.path.sep.join( + [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] +) +pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): From 009615cf1855ccc992c783f09b1b3c8a4fbcde5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:56:58 +0200 Subject: [PATCH 080/193] removed original resources --- .../resources/original/delete-button.svg | 55 ------------------- .../resources/original/information.svg | 44 --------------- .../resources/original/picture.svg | 48 ---------------- .../resources/original/play_icon.svg | 19 ------- 4 files changed, 166 deletions(-) delete mode 100644 pype/tools/standalonepublish/resources/original/delete-button.svg delete mode 100644 pype/tools/standalonepublish/resources/original/information.svg delete mode 100644 pype/tools/standalonepublish/resources/original/picture.svg delete mode 100644 pype/tools/standalonepublish/resources/original/play_icon.svg diff --git a/pype/tools/standalonepublish/resources/original/delete-button.svg b/pype/tools/standalonepublish/resources/original/delete-button.svg deleted file mode 100644 index 48b09ac787..0000000000 --- a/pype/tools/standalonepublish/resources/original/delete-button.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/information.svg b/pype/tools/standalonepublish/resources/original/information.svg deleted file mode 100644 index c040bab773..0000000000 --- a/pype/tools/standalonepublish/resources/original/information.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/picture.svg b/pype/tools/standalonepublish/resources/original/picture.svg deleted file mode 100644 index 35f912ce80..0000000000 --- a/pype/tools/standalonepublish/resources/original/picture.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/play_icon.svg b/pype/tools/standalonepublish/resources/original/play_icon.svg deleted file mode 100644 index e9bab5a251..0000000000 --- a/pype/tools/standalonepublish/resources/original/play_icon.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - From f6b6f730a8ae2b3abf043d6a26a34ec4719ccb97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 11:47:23 +0200 Subject: [PATCH 081/193] moved standalone publish from tools into pype --- .../{tools => }/standalonepublish/__init__.py | 1 + pype/{tools => }/standalonepublish/app.py | 18 ++++++------------ pype/{tools => }/standalonepublish/publish.py | 2 ++ .../standalonepublish/resources/__init__.py | 0 .../standalonepublish/resources/edit.svg | 0 .../standalonepublish/resources/file.png | Bin .../standalonepublish/resources/files.png | Bin .../standalonepublish/resources/houdini.png | Bin .../resources/image_file.png | Bin .../resources/image_files.png | Bin .../resources/information.svg | 0 .../standalonepublish/resources/maya.png | Bin .../standalonepublish/resources/menu.svg | 0 .../standalonepublish/resources/nuke.png | Bin .../standalonepublish/resources/premiere.png | Bin .../standalonepublish/resources/preview.svg | 0 .../standalonepublish/resources/thumbnail.svg | 0 .../standalonepublish/resources/trash.svg | 0 .../resources/video_file.png | Bin .../standalonepublish/widgets/__init__.py | 0 .../widgets/button_from_svgs.py | 0 .../standalonepublish/widgets/model_asset.py | 0 .../widgets/model_filter_proxy_exact_match.py | 0 .../model_filter_proxy_recursive_sort.py | 0 .../standalonepublish/widgets/model_node.py | 0 .../widgets/model_tasks_template.py | 0 .../standalonepublish/widgets/model_tree.py | 0 .../widgets/model_tree_view_deselectable.py | 0 .../standalonepublish/widgets/widget_asset.py | 0 .../widgets/widget_asset_view.py | 0 .../widgets/widget_component_item.py | 0 .../widgets/widget_components.py | 0 .../widgets/widget_components_list.py | 0 .../widgets/widget_drop_empty.py | 0 .../widgets/widget_drop_frame.py | 4 ++-- .../widgets/widget_family.py | 4 +--- .../widgets/widget_family_desc.py | 0 .../widgets/widget_shadow.py | 0 pype/tools/standalonepublish/__main__.py | 5 ----- 39 files changed, 12 insertions(+), 22 deletions(-) rename pype/{tools => }/standalonepublish/__init__.py (60%) rename pype/{tools => }/standalonepublish/app.py (95%) rename pype/{tools => }/standalonepublish/publish.py (95%) rename pype/{tools => }/standalonepublish/resources/__init__.py (100%) rename pype/{tools => }/standalonepublish/resources/edit.svg (100%) rename pype/{tools => }/standalonepublish/resources/file.png (100%) rename pype/{tools => }/standalonepublish/resources/files.png (100%) rename pype/{tools => }/standalonepublish/resources/houdini.png (100%) rename pype/{tools => }/standalonepublish/resources/image_file.png (100%) rename pype/{tools => }/standalonepublish/resources/image_files.png (100%) rename pype/{tools => }/standalonepublish/resources/information.svg (100%) rename pype/{tools => }/standalonepublish/resources/maya.png (100%) rename pype/{tools => }/standalonepublish/resources/menu.svg (100%) rename pype/{tools => }/standalonepublish/resources/nuke.png (100%) rename pype/{tools => }/standalonepublish/resources/premiere.png (100%) rename pype/{tools => }/standalonepublish/resources/preview.svg (100%) rename pype/{tools => }/standalonepublish/resources/thumbnail.svg (100%) rename pype/{tools => }/standalonepublish/resources/trash.svg (100%) rename pype/{tools => }/standalonepublish/resources/video_file.png (100%) rename pype/{tools => }/standalonepublish/widgets/__init__.py (100%) rename pype/{tools => }/standalonepublish/widgets/button_from_svgs.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_asset.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_filter_proxy_exact_match.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_filter_proxy_recursive_sort.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_node.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tasks_template.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tree.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tree_view_deselectable.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_asset.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_asset_view.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_component_item.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_components.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_components_list.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_drop_empty.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_drop_frame.py (98%) rename pype/{tools => }/standalonepublish/widgets/widget_family.py (98%) rename pype/{tools => }/standalonepublish/widgets/widget_family_desc.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_shadow.py (100%) delete mode 100644 pype/tools/standalonepublish/__main__.py diff --git a/pype/tools/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py similarity index 60% rename from pype/tools/standalonepublish/__init__.py rename to pype/standalonepublish/__init__.py index 29a4e52904..41bc69b9a2 100644 --- a/pype/tools/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -1,3 +1,4 @@ +from .asapublish_module import ASAPublishModule from .app import ( show, cli diff --git a/pype/tools/standalonepublish/app.py b/pype/standalonepublish/app.py similarity index 95% rename from pype/tools/standalonepublish/app.py rename to pype/standalonepublish/app.py index f45c8bd0cd..1a875505d7 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -10,6 +10,8 @@ from .widgets import * # Move this to pype lib? from avalon.tools.libraryloader.io_nonsingleton import DbConnector +module = sys.modules[__name__] +module.window = None class Window(QtWidgets.QDialog): _db = DbConnector() @@ -22,7 +24,7 @@ class Window(QtWidgets.QDialog): NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(Window, self).__init__() self._db.install() self.setWindowTitle("Standalone Publish") @@ -172,18 +174,10 @@ class Window(QtWidgets.QDialog): data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) data.update(self.widget_components.collect_data()) - + return data -def show(parent=None, debug=False, context=None): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - - """ - +def show(parent=None, debug=False): try: module.window.close() del module.window @@ -191,7 +185,7 @@ def show(parent=None, debug=False, context=None): pass with parentlib.application(): - window = Window(parent, context) + window = Window(parent) window.show() module.window = window diff --git a/pype/tools/standalonepublish/publish.py b/pype/standalonepublish/publish.py similarity index 95% rename from pype/tools/standalonepublish/publish.py rename to pype/standalonepublish/publish.py index bb6c7215b4..215281bfaf 100644 --- a/pype/tools/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -23,7 +23,9 @@ pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): os.environ["AVALON_PROJECT"] = project + io.Session["AVALON_PROJECT"] = project os.environ["AVALON_ASSET"] = asset + io.Session["AVALON_ASSET"] = asset io.install() diff --git a/pype/tools/standalonepublish/resources/__init__.py b/pype/standalonepublish/resources/__init__.py similarity index 100% rename from pype/tools/standalonepublish/resources/__init__.py rename to pype/standalonepublish/resources/__init__.py diff --git a/pype/tools/standalonepublish/resources/edit.svg b/pype/standalonepublish/resources/edit.svg similarity index 100% rename from pype/tools/standalonepublish/resources/edit.svg rename to pype/standalonepublish/resources/edit.svg diff --git a/pype/tools/standalonepublish/resources/file.png b/pype/standalonepublish/resources/file.png similarity index 100% rename from pype/tools/standalonepublish/resources/file.png rename to pype/standalonepublish/resources/file.png diff --git a/pype/tools/standalonepublish/resources/files.png b/pype/standalonepublish/resources/files.png similarity index 100% rename from pype/tools/standalonepublish/resources/files.png rename to pype/standalonepublish/resources/files.png diff --git a/pype/tools/standalonepublish/resources/houdini.png b/pype/standalonepublish/resources/houdini.png similarity index 100% rename from pype/tools/standalonepublish/resources/houdini.png rename to pype/standalonepublish/resources/houdini.png diff --git a/pype/tools/standalonepublish/resources/image_file.png b/pype/standalonepublish/resources/image_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_file.png rename to pype/standalonepublish/resources/image_file.png diff --git a/pype/tools/standalonepublish/resources/image_files.png b/pype/standalonepublish/resources/image_files.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_files.png rename to pype/standalonepublish/resources/image_files.png diff --git a/pype/tools/standalonepublish/resources/information.svg b/pype/standalonepublish/resources/information.svg similarity index 100% rename from pype/tools/standalonepublish/resources/information.svg rename to pype/standalonepublish/resources/information.svg diff --git a/pype/tools/standalonepublish/resources/maya.png b/pype/standalonepublish/resources/maya.png similarity index 100% rename from pype/tools/standalonepublish/resources/maya.png rename to pype/standalonepublish/resources/maya.png diff --git a/pype/tools/standalonepublish/resources/menu.svg b/pype/standalonepublish/resources/menu.svg similarity index 100% rename from pype/tools/standalonepublish/resources/menu.svg rename to pype/standalonepublish/resources/menu.svg diff --git a/pype/tools/standalonepublish/resources/nuke.png b/pype/standalonepublish/resources/nuke.png similarity index 100% rename from pype/tools/standalonepublish/resources/nuke.png rename to pype/standalonepublish/resources/nuke.png diff --git a/pype/tools/standalonepublish/resources/premiere.png b/pype/standalonepublish/resources/premiere.png similarity index 100% rename from pype/tools/standalonepublish/resources/premiere.png rename to pype/standalonepublish/resources/premiere.png diff --git a/pype/tools/standalonepublish/resources/preview.svg b/pype/standalonepublish/resources/preview.svg similarity index 100% rename from pype/tools/standalonepublish/resources/preview.svg rename to pype/standalonepublish/resources/preview.svg diff --git a/pype/tools/standalonepublish/resources/thumbnail.svg b/pype/standalonepublish/resources/thumbnail.svg similarity index 100% rename from pype/tools/standalonepublish/resources/thumbnail.svg rename to pype/standalonepublish/resources/thumbnail.svg diff --git a/pype/tools/standalonepublish/resources/trash.svg b/pype/standalonepublish/resources/trash.svg similarity index 100% rename from pype/tools/standalonepublish/resources/trash.svg rename to pype/standalonepublish/resources/trash.svg diff --git a/pype/tools/standalonepublish/resources/video_file.png b/pype/standalonepublish/resources/video_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/video_file.png rename to pype/standalonepublish/resources/video_file.png diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py similarity index 100% rename from pype/tools/standalonepublish/widgets/__init__.py rename to pype/standalonepublish/widgets/__init__.py diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/standalonepublish/widgets/button_from_svgs.py similarity index 100% rename from pype/tools/standalonepublish/widgets/button_from_svgs.py rename to pype/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/tools/standalonepublish/widgets/model_asset.py b/pype/standalonepublish/widgets/model_asset.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_asset.py rename to pype/standalonepublish/widgets/model_asset.py diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/standalonepublish/widgets/model_filter_proxy_exact_match.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py rename to pype/standalonepublish/widgets/model_filter_proxy_exact_match.py diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/standalonepublish/widgets/model_filter_proxy_recursive_sort.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py rename to pype/standalonepublish/widgets/model_filter_proxy_recursive_sort.py diff --git a/pype/tools/standalonepublish/widgets/model_node.py b/pype/standalonepublish/widgets/model_node.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_node.py rename to pype/standalonepublish/widgets/model_node.py diff --git a/pype/tools/standalonepublish/widgets/model_tasks_template.py b/pype/standalonepublish/widgets/model_tasks_template.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tasks_template.py rename to pype/standalonepublish/widgets/model_tasks_template.py diff --git a/pype/tools/standalonepublish/widgets/model_tree.py b/pype/standalonepublish/widgets/model_tree.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tree.py rename to pype/standalonepublish/widgets/model_tree.py diff --git a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/standalonepublish/widgets/model_tree_view_deselectable.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py rename to pype/standalonepublish/widgets/model_tree_view_deselectable.py diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_asset.py rename to pype/standalonepublish/widgets/widget_asset.py diff --git a/pype/tools/standalonepublish/widgets/widget_asset_view.py b/pype/standalonepublish/widgets/widget_asset_view.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_asset_view.py rename to pype/standalonepublish/widgets/widget_asset_view.py diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_component_item.py rename to pype/standalonepublish/widgets/widget_component_item.py diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_components.py rename to pype/standalonepublish/widgets/widget_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components_list.py b/pype/standalonepublish/widgets/widget_components_list.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_components_list.py rename to pype/standalonepublish/widgets/widget_components_list.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_empty.py b/pype/standalonepublish/widgets/widget_drop_empty.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_drop_empty.py rename to pype/standalonepublish/widgets/widget_drop_empty.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py similarity index 98% rename from pype/tools/standalonepublish/widgets/widget_drop_frame.py rename to pype/standalonepublish/widgets/widget_drop_frame.py index 1c9c9ea359..90434e75f4 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,7 +11,7 @@ class DropDataFrame(QtWidgets.QFrame): super().__init__() self.parent_widget = parent self.items = [] - self.presets = config.get_presets()['tools']['standalone_publish'] + self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) @@ -267,7 +267,7 @@ class DropDataFrame(QtWidgets.QFrame): icon += 's' data['icon'] = icon data['thumb'] = ( - ext in self.presets['thumbnailable'] and + ext in self.presets['extensions']['thumbnailable'] and data['is_sequence'] is False ) data['prev'] = ext in self.presets['extensions']['video_file'] diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py similarity index 98% rename from pype/tools/standalonepublish/widgets/widget_family.py rename to pype/standalonepublish/widgets/widget_family.py index 26dab9bc19..7259ecdb64 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -247,9 +247,7 @@ class FamilyWidget(QtWidgets.QWidget): def refresh(self): has_families = False - presets = config.get_presets().get('tools', {}).get( - 'standalone_publish', {} - ) + presets = config.get_presets().get('standalone_publish', {}) for creator in presets.get('families', {}).values(): creator = namedtuple("Creator", creator.keys())(*creator.values()) diff --git a/pype/tools/standalonepublish/widgets/widget_family_desc.py b/pype/standalonepublish/widgets/widget_family_desc.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_family_desc.py rename to pype/standalonepublish/widgets/widget_family_desc.py diff --git a/pype/tools/standalonepublish/widgets/widget_shadow.py b/pype/standalonepublish/widgets/widget_shadow.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_shadow.py rename to pype/standalonepublish/widgets/widget_shadow.py diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py deleted file mode 100644 index d77bc585c5..0000000000 --- a/pype/tools/standalonepublish/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import cli - -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) From 1c777e36c0cb45b566944e6da33aced9f9666616 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 11:47:41 +0200 Subject: [PATCH 082/193] standalone publish is tray module now --- pype/standalonepublish/__init__.py | 3 +++ pype/standalonepublish/__main__.py | 5 +++++ pype/standalonepublish/asapublish_module.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 pype/standalonepublish/__main__.py create mode 100644 pype/standalonepublish/asapublish_module.py diff --git a/pype/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py index 41bc69b9a2..cc6f33d47e 100644 --- a/pype/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -7,3 +7,6 @@ __all__ = [ "show", "cli" ] + +def tray_init(tray_widget, main_widget): + return ASAPublishModule(main_widget, tray_widget) diff --git a/pype/standalonepublish/__main__.py b/pype/standalonepublish/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/pype/standalonepublish/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/pype/standalonepublish/asapublish_module.py b/pype/standalonepublish/asapublish_module.py new file mode 100644 index 0000000000..d695065601 --- /dev/null +++ b/pype/standalonepublish/asapublish_module.py @@ -0,0 +1,15 @@ +from .app import show +from .widgets import QtWidgets + + +class ASAPublishModule: + def __init__(self, main_parent=None, parent=None): + self.main_parent = main_parent + self.parent_widget = parent + + def tray_menu(self, parent_menu): + self.run_action = QtWidgets.QAction( + "ASAPublish", parent_menu + ) + self.run_action.triggered.connect(show) + parent_menu.addAction(self.run_action) From c5a0be4ce0dcc1fde632763f8cedfbe4f10e8665 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 20:50:42 +0200 Subject: [PATCH 083/193] added icons into resource in pype --- res/app_icons/Aport.png | Bin 0 -> 5865 bytes res/app_icons/clockify-white.png | Bin 0 -> 16188 bytes res/app_icons/clockify.png | Bin 0 -> 5955 bytes res/app_icons/djvView.png | Bin 0 -> 4032 bytes res/app_icons/houdini.png | Bin 0 -> 262950 bytes res/app_icons/maya.png | Bin 0 -> 41557 bytes res/app_icons/nuke.png | Bin 0 -> 49012 bytes res/app_icons/premiere.png | Bin 0 -> 20121 bytes res/app_icons/python.png | Bin 0 -> 7821 bytes res/ftrack/action_icons/AssetsRemover.svg | 1 + res/ftrack/action_icons/ComponentOpen.svg | 1 + res/ftrack/action_icons/CreateFolders.svg | 51 ++++++++++ .../action_icons/CreateProjectFolders.svg | 51 ++++++++++ res/ftrack/action_icons/CustomAttributes.svg | 1 + res/ftrack/action_icons/DeleteAsset.svg | 1 + res/ftrack/action_icons/JobKiller-512.png | Bin 0 -> 14539 bytes res/ftrack/action_icons/MultipleNotes-512.png | Bin 0 -> 21216 bytes res/ftrack/action_icons/RV.png | Bin 0 -> 10126 bytes res/ftrack/action_icons/SyncToAvalon-512.png | Bin 0 -> 4835 bytes res/ftrack/action_icons/TestAction-512.png | Bin 0 -> 14386 bytes res/ftrack/action_icons/new/plus.svg | 1 + res/ftrack/action_icons/thumbToChildren.svg | 88 ++++++++++++++++ res/ftrack/action_icons/thumbToParent.svg | 95 ++++++++++++++++++ 23 files changed, 290 insertions(+) create mode 100644 res/app_icons/Aport.png create mode 100644 res/app_icons/clockify-white.png create mode 100644 res/app_icons/clockify.png create mode 100644 res/app_icons/djvView.png create mode 100644 res/app_icons/houdini.png create mode 100644 res/app_icons/maya.png create mode 100644 res/app_icons/nuke.png create mode 100644 res/app_icons/premiere.png create mode 100644 res/app_icons/python.png create mode 100644 res/ftrack/action_icons/AssetsRemover.svg create mode 100644 res/ftrack/action_icons/ComponentOpen.svg create mode 100644 res/ftrack/action_icons/CreateFolders.svg create mode 100644 res/ftrack/action_icons/CreateProjectFolders.svg create mode 100644 res/ftrack/action_icons/CustomAttributes.svg create mode 100644 res/ftrack/action_icons/DeleteAsset.svg create mode 100644 res/ftrack/action_icons/JobKiller-512.png create mode 100644 res/ftrack/action_icons/MultipleNotes-512.png create mode 100644 res/ftrack/action_icons/RV.png create mode 100644 res/ftrack/action_icons/SyncToAvalon-512.png create mode 100644 res/ftrack/action_icons/TestAction-512.png create mode 100644 res/ftrack/action_icons/new/plus.svg create mode 100644 res/ftrack/action_icons/thumbToChildren.svg create mode 100644 res/ftrack/action_icons/thumbToParent.svg diff --git a/res/app_icons/Aport.png b/res/app_icons/Aport.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6816513a46c3000c9f5341c2ed3f4ca8e91523 GIT binary patch literal 5865 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 V)-2l>T}irx*|}`JwuSxUe*rlRkFo#& literal 0 HcmV?d00001 diff --git a/res/app_icons/clockify-white.png b/res/app_icons/clockify-white.png new file mode 100644 index 0000000000000000000000000000000000000000..2803049fbe53b1d4915a1fea061a4a3ac5fbfa29 GIT binary patch literal 16188 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 SL?(OY4b08XnpPOQ$NnG5MH@>1 literal 0 HcmV?d00001 diff --git a/res/app_icons/djvView.png b/res/app_icons/djvView.png new file mode 100644 index 0000000000000000000000000000000000000000..854604d57f02640b6f4e1a380301769b625745c5 GIT binary patch literal 4032 zcmW+(2{@GB+kQi`42C3Wl7xnaMvXQ5R`xJs3E8vnvNR%lD6)<%`w&Xm_pJ%}kMh%G zmmwh}G-Qc<$9G-tdfszA=Q;O#?)$k<{2hZ^Y%F{%5CpMl-_|e&cl7be#0aitbbE0S z#1W^hp^6R8`*_#g=kOlR z3S-{k&$B0m>g$QlLsy@PPf^=x?bMwao6`r98o%)6I~_+P&eMs#&E}{+Bb|XL9wVLj z26l)R^j=0dC0OB8;DZPHCre@71y&Mc9t5(~`R)Rd&Z|Y$)e<87$#uKSrD2LssD362 z{LXkho^7>`y8dSSVFx)l--ZjuXelNpc7HFllck)p%mCqVBUTcG0mthyl;y3M0Qo~t zrGg`O`4{|;;6utf&~@KzYisMlQx`)wLY4>+;4sFy&ERhI z4tqaF|3fWI6~1w}L%p1G+t$Yi9lkc%FhAh%tutzJ8Bh9Iwm-Z+;JUqnWaMoiF z6w?N>DzFJGkt%5eLLZlCpfyKN#Pxc1X=UZ-P$fgqdWIpQ%vdyMz)=tM$KkgB{R^8M z9#$pIZOrY_mJF|jEnXYGR(S`-GrzyTzt!Tu)K^(m#nHaMQYEz+JZ)={`Q1;Bu{lUA z8lF|g{AMiis#*1HNwU(rjH_l4w7tD;-W8^}vbvfww4{q=g+i4IDputWzo!|F?@+^! z?qAP77@M1$OB<~VC*>b8@I!ojeD|yx+%g{SZ@KD-j%XN_Mhg5FAn$1dZfZp;h6PIc z1aV6(OH0e;V$H-K;B;|!ziQp+aZd5kK1Q6I#boAcf*>*7jh>!9V5Ab~rmjv(?B(Ux z;MnTj49;6yU8St9tc+r0WG+s&1znQ~{}*~AL&rB7jz8>p%Wl|N(__p{l*ZJ4iHiEmCmlD>@Y{%Zlrr@8yv?LMA)OB}p z39hZF*(~|m7L?o2(13Yia04L|qOM1Yo?7eM*g$J(Y0c913SIe-#KLqn3T8J{>FLv_ zw!y)?UQJV`1@_-$*_Q)yDqg=Pwgs(ApFe;8sH(Fw%+=fb&8EPmGOowVYimStaq$rx z4j0$f+KK>UJQs?$<7JGs8$U}_4!Id?C!oy*oA0TfM#CKICQ?rqre|*C&62*1juux{ zk&UdaAH5_JiLFgd1cR8M&u0NH5L!zAukNi|{qw^v*rveF_IAXXGiUzvqG3p)K(xx( z1HA&J3lfAXjkorN5(HLCxnW%OszCH(W0qJuo#0FYVd@I!Gp+5Uq@;cjh!yguvt-Wr z5k-`jl9E~@XNv!&bL=SPwkB8!oCxKAE0 zm|92XB!0z&l@&Kjecsp}0ORn?Oj2vB{D)c()>0&k(&3hgReLZeid@{#aHhr_h9awR zYkGamk=m~?DgT!@v@`{{pFDH1W}Wcly}hJ#!O)Snp`oGUOnb=U?yhW=D_`Kx@rce& zrN6sur0&y(3K2~+4_N`6AOZF``Uwx7sV47R;o-{WnGItS_u6@nUqeFVF#1vCg?HoQ zyp5hyvHnY+CfxNY8YmtTc%Ai;y<=*6KXm77(4hO9j?9gR9jt&aFm6pZOGvl2|6=&T zUW5DCzoAbrZn9~on&&Uo_%9hCgem|Ld*(7 z=PjGOoB#?o%^|c!=iYMLsyniXGUyH<_{z#il_&}QWjWy6=xa+^-){_b5i0R>e6bO> z=qERvKiRstFs0mPs70w$FmScEZX;s#O-)n#1w%_~R0aOz430~1CKXD30roMRf$l_} ztS=oHR2_#q!y8*vW%j15Kj{K~8xW+x!aSnjY1Z+koB>L>`X?&nZGfHIxzaztK*YNZM}UOxwp+o_IGH}p><`sV<6(bb0RwV8Iw z$;tD8c0TeiKHRVK06JVzRb|N!V|3;b4%@kBU&Bie@h2};oMhz*J_zdqj3`Qfw?yli z^U-_wyn`M_pTFek?Z`>46*k*s&n;ulU%GO#-ri3Zv9TALnJXNj(rw=CQ`A4ddEhU( z$i)r_plf;eZZ{ex99?(^Y5OTp&c)45O*%J*H20RqA4fW2ktylFsHY*w$;}eObwZC) z{|N8GD_*|L zItODcsuXirdx9dnxVWeo8gj7nD?BMGx|CHG4|1F(67H$_`_oEcb+2B*)Ya8@4`@TG zOsaVN&79m^PfL4V#=TH}PfySBChv<%|8^aKZauc+jV9T>vgzUnR2Ob6uM|5CV>B`{ zl4N}x2N$FlFpbeXp}>>e|K=yPi`rj{5|{X-Pl*P6rz}ScX&0tPE`{*M0wi^hXLwBO z^DkRXtIYJUoOlh80ME=EY()`AC6y2YxR0S4VjCV7KSel1p7nNBt}W;OhN z%b$fqcjw}oL-(%hQ8YX~#Q|gEVpz{kYI#&>$RhgM);kkRZY9$}lT%Zi>~WqYD8}%` z!E*?OV0cMM2?)%_#umJ{Kmcpdk=F3#3aD!YvJ>Qp`VRr=hzdu;<I{H~?t2?(G? zw@%yJ_$WgytgE90azQ=^yS^}X_HQqOW>kf^dO%;O8x*uifw$Ruiy}}6-3=$mcJKEB z3M!>6(?Fhp&tDQwTZp}<5 z3mqmf<=u2`!pXk>VG=vya3VQnuI1dlU6QaUD+y<+@|oq zK0ydAE&XRE~TLQ9`sMA`8&#i$)S3Z_eenhSw#H4j!z5jO*2px6f{#yD9# zefG?*H{r~9i~kjt7`*R?IiJVI2`1$RcAy9%`ip?S7N@rySb_f$rA&X?%;wRE zlDzzW@3qFc*NnzU?iEF!SqKahDsKG;?}L{jSUUMpxw!$ab35Zf~8<`-xMQ340&m3Em7iR1pO%&B=`Qh zy+V_)2|5iR32EdWN$6B99i_O-813xmPvS%m4NMjt;*N$F3_ zp8RzVF91ht-MTdZyi69@1Z@9*1n&#Vhbw%Rc%^YuL! zjQmF7B^&8nDF#(EWw}1vpj=-SKIkER)Ad8p!R{ti@%!J84yHPy*;4MKti2ASw}m1n zCnpOo)Bzj(4d_j>$KXEl)K}e*{P53w0r-v?76N&J0n#-35~_3*2Wt)II8;9vnKc?d knkrEIha5Qa3UAs|mWQvbr8@bez|I`f)-=$lR*{0AxK<>)HV-9FKidFb zR=lqN34JRrEmOBfohIk>eds-rX41b;uT%b^q}j;DBXEZ;mI9UnmI9UnmI9UnmI9Un zmI9Un8A^fO{mN!&%WbnQ1u}yI`R6!w3wxE^8?yKM(tfZMuoOrK3T!Sjao1k$D7V)q z_X8(?P66NBuPg;D1yY*=RZ#uSu}vrw`@vpY3Rntc76mK-WLAIM)@C{dt~{dWJkJ&& zA1y2xcb*y*;(mI59HEC6_vuxCqw45WYs zfDG((+hj`tj{+6|JWANJr9cK!zyd%9cDikH22kLpa-+v4XbX@>jU^+Rjo zDUf*-umF&GeQsN9DUe423jlefuospBnMVN&0GZe4w#AkLc@(e!kVgu8VJVP#6tDo0 zd3|nMY$=dO0Sf?mq_7v30+~kv3jmqd=eEUZN`ZT()LNgcEkGQo~+b3S<}sEC6I!huh{_ z3dB;t0zfP^?6svphEc!*K!$a=ZEpHepxa~1>z2kAz@vx-1CJ8+Y$=d|6tDo0ft_xf zY$@PTpqzJM&-o~zwTRx01K$^P@OFOB;+@@3@&BsauXy$je#L7-u=5=*6fbPC6bO$3 znGFD(PW6zcFU9ASrp{hgNs$NH=YE6+UI4V~asl+2&q#|xCR}n{fOy;tC zdXC_+a|ez;MYiMkbBoBwd-GcDcVND0sl&Tw8G1 zLs)y|Cj~MG05~Ccal(Uq-ccD7wrP7&ja!Q}Y9Ug;xk!ViB6at5Cp#x}S&*m<4_@O? zn2cpOA-%J+eCMv-u`75kTad>|x^6{*}BG#4XWJ0Z6h76(;wvYlkR@i@-I3Vd*@PMRPs(K9|4S+g>RLm(>Hl1>!Zb!&EKu(Hy>Pp?x-(5@5*0}lYm-e;8;)$u5i zZv92N^aE?(2$Mf|wI^Gjsl@{aa!1r7%=k$3*YAmtTOe>coC4*CxOE^?DV9m?PC)?h zCOsBAsaaiAm%bv04aTaE$w zEWN);Apqc3-A=H(RjP0FWbpBFvp71$tvOK@x-tgax9`rI%8;J||n?h+_~D7JMZ#{t?kr#seGx5Y`g0G}hS8J6)}VL_ zkSkcZSY+J8qCXr39|zc#SRg2#ZUNmyH89xmr>jBq^ZE8!eo!DF0N|cc z&4wZ;TqFw~GF1g1g5^{g2)9F{K}U1^`ZchR-?TWKn0| zYSsLvbh8Qd_~w1lqoAbz8RmxEPqf7Z3it>BjFknSTy_54BCQWHp>ehjrcr<~(*FBz z(c>Nw*?`>xI(%3QqbPf+v;Y7kXK*~&?W!|}i}b{no-LLF#ZlnbpIxzF>RTe7e!v1j zaa4=_`>2ij#)54DV(&`sx`bdrc8*5lHR?gbf2OFv+>WT3S;@WH-fbyB+v8X7KoElk z@Cbv&m<5Ee$3zJLfRmr$a`)>Z>cacM`gaMt()NAnM1kK|iM(=$=ubww;glWQW9XG^ z34;Pj0029NkvQt^BXZJ}q6S|bMg`4&2esSuZgOIdqH1GFN&oZ_81sBt(aa8F$A z9M`*C_ec6YXYva8<#?AHpnD<%7+AaG-0>6WDGmVW8oqbo0O!D@roY2sqAq_J`}{5U zz9(HjaWXS9=MIF|VTc{Z-tt2EZiT@MgZ=-p&JDf8Sl|3^8>evIKv6f$X?I1>J>%qjjy>}_ws;Id?3vfV8t1oF0T7I1UJrp18Z<}1 zgcc(E><6W5Yd48O1!O4UQL(ZM60y%t^=opM*FGz%eRt7g9~Ai&bmszO14M+@vsmi) zE_&h2Y^*{#%&d`Cg%f<+X7^wkA14_9Y>~y^iC!`f6CZ;Qi+^5*4o>#G36G1QRwjxL z6e&DVATaP?sas1_^Nu1dI*GJ7ScKPAYrEV;7UQOYb4ZthIOrP2L^A7BaA4>N3j#s( zY@7%X4**(Y`A@+F0{O{B_MLX>%-gl$H#g8e9N+a1-yjRe_h9ew1I{0BqL=k%d;SMS z_}S4l02#0-I`xHQpjvi7;KU*z+GWE;hn?4$(~4udfWoR3-~5=5ki?s z6An2ZyEKreYu0yl3#q<(zW0pi|NAFIf}dRFYUU~kSC^@Ey+}cE0TLN6Mcn#4^R0I#oq=9RPRilQDQ8Hq>t9 zDj*Ykl6iMQEYPpqhCZF@DrarvSU>tGO#pz(JO->-|0^5}bvkqh&?D4svgaP-uK?!M2T-5$^6jE0 zKJV%X98m|~+fot=q#yurpPrMR`LLJ*=ZiOxIdhpaHl#uZuJp-tJGp}Gm`D(X)R6;4 z`kpJ&sd$D~BMWNFeG{}}@D3AgG zKv{43k0NuR&7brt@@0eF=ayJSj@}HD>yzo)MDF!~0D2PD_b=cGa1aQxt0(enGwzx4 z7L>; za5&&tDA$0J8e9%o9eI`RFBpz(RNXt~~efYzdSC zG{MDgskG_fI({)*YpN&;=T8&;{7ta?_yP?`I}mth*VJs0U~B=rA!QI4a7AK_If4K) zR4kltLBQ*#u+KXI3Y<@U&lDMOu}H@w&E~*3cE}_%l6SLF0W9`59JehYvPeVd#Ot;yI$9z7AsKB(yc{xPrv{FR?o# z1^~L&&iKfc_o?Ck6Dv;zEAJ=PcOl(Hdjb^I?>yvvyG*2QH|MjN|6syXX+#kC&o!=t z5i4r-M}>7)gaAP0&8MS9;c zKhF~V@2g-XI++MyL7?Cf6afIxeE0J=VAb^kw)bYb7JTXC-dZrB)8kv}2CCM8NB|bQ z|4^70H!!qaxghWZyg>hTC9FojMLjJD6wp~A1HkOhM8E%x2#tC+L-kzIYWWN36tll_ zB1e@$3g~l127}FZ^1qu{M^O>gEyD-7J9F36iH;WwD)9kuWhtJ5r00O@)vmmf{PsITM{pkEH+?shC z_S;|l9~?P0BkY)6_5CGq1W)5iS{48RO}qYm4Z_8bboBxeR3)KTB5s0$6PbSU?{3V9 z)KLn~EqB-XMc4w2)FToiKq+qW4-x3UOX2_Rf5FCgn(MQh4l)2WWKM`+P#Kp_@y@v) z^G=XMox-zo;GcBiOF66PrpH)oP{Av1@zCMG)Y9i1QAl^JUl{JjB1;`q2$iD#0Kmp| zqQ^ffLe1Zb=BY2jFs}fb@4Dajst@5`x z2!KxL;ccR~ZOsi*xDmdt(5&$}m^%(A@a!D96AB+wrf?EbqYJ|h{oIYB7>v>62(yTJ zuh#u48adCMKCmDVH`GcG0L#7?jaqTac*a2jODKcwYZw~AV|81AOy z&8%a@q$(W%%!H80oKQ1AcL%V^=6$qy!6NM(*=0qwXooo6ZA2Qk5~<$|6CZnC%#l{D zjvL^g2^9R)WRhiOw3ChNMR{Prxa4b=Ay)Y!(F~2lV0k&HC&7R{!#HOqPApW-;RKLO zL65#$6uGA%!}zUvB^%t^MfZcoixEU}wz>3-h;nhTtrHC&46h#j#(gl*o)66*dD^%~ zMbxC2*TzozMh!$U|5>X}B2C)55i=OTo(}U|)uSp5aZHYYV6YBRQ3wQxZLaA(we)*8 z><__!ho6AtS9?igWJFP?-zY)@0#gh3sP7yCt_xdMCXUr`=oYsv5I1X!1%TZ<5!w|p z6+^eI#DFzT(MNfHCsrhK*A454>TtM|3du=`fKqg1o z`iPn$e{9L+w9suPg$O1HKx9&9-)go_V8T-{8h~$f=9W?d0AySJ(g=|WPlJ_(Tx$}` z@5RO<*WB??5&FF{M^%$H00K<>R4rR`pmYQKx{@p`B4jW^D*agH#T<9)7$`fwHjttSH`xM;gy&F05d+s)cRyJ zu9vzB#B$tP2)PghSPni$)D@2-GiodMvuXdneh;|Yo6HhvgQroF0DwJh{qRMOwVz`d z7xjMi4zQb;`|2>DH)YQDJJ2c>U0(TP$U{h}5cb&>!8q;=H~UFbOYM?U!}XOdgnzq1?dvU6`&vmYAW zQtU-ENMelb=@Ve4ja7KfDu@OO$O&y}ScQF78PQ2kyFuj4;bFZsx%VubDf-$Ypx??E zQ6ND8&^kvycA3Zr&tt{i8ScQz--R@*ty5Fv#J{?-ezOi{=pgdf)5OoTO8jMk$j5No z{O=QxdC`|)?&1&x>LdQxMGuHDr-{>~*3P*t8zTTmKXN`n@7wf8ufjq3Cj;57>oHJm3gdAJF2! zL~9)0eGr@U4SgK?L^jsLYi$AmFt}DWSpFQzfjk+?Aj#i)44N$%H2dxgE3;vc_o3ad z(>VW4_H#)num;M($>U(xaVw&?z}6rPB_VffV1=iS9V&7;^Z|6PGOf(PuE2FiWddgz z2LSOx;U?IwQ<)USTvz>uh#Edd)NxQi+R5+Vbg9=AQ9v&hb@xQX1wR~m70}z+ zrt|L=aZe%#38kQ>;}}Is6HA94EzLbNFE=J5{pEJzS2hj-1IL{7e|R{TJ?gcPq+X;b5Z*6A9K2>8(z-MRvB&;f*@krJQG})8uk*LtYCH> z6bMkkyi>cOyJmfA9_bhp^6Z&glBqnj`a`Z2b=7|$bG9(A^KFgbXVFm9 zHP6A_`k3HApge}%!tX?;j>j&9VJ5i*s7}4HICTXVfH9lq1+5LCP`QL~QgQ*}$_<5O z8kJABm`ee~O;pzc1Uh0XHW0-iSZ$0vJ@vnEYyQwYI>7qW@n?WhtpBCS$}x4#vIX$k z7(5uzv#)*=kt-e(b@DZi_n7x`dp4K?b@qYC_6mZlwt|^2`q1w}>%V1|;U2N-;2n8Ur(N06Mt77R6KueC@V2#{g zGV7B>Y7_1~S>Ol;{SD$uxUUWSZNUKmSAOQCzV3NwnU4*-UG{x`P=FfpmgvLlM`0oa zpdjNG;i_K|*8n1cXeRv$>92|RLFyQAy_565*{{XR7U;N)*{fV+>}v|T#V$+n3UAWM8CLbG={!sitc%0`x%*bQK4Gm{E+Ak08jzdLdn;}_g_#PD|w;SM1!WHE&TtY&X z3;;M0;8-okUj|>_3rhHEgMY>ePq#*LI9$=cn~aU?snA?~<)--E!9kDuJ4fz?v%)8_ z8ZxBs^mjy0fw6o=Bttx?kI0~_ktE?TSNP6D5<}_KpgDp&UMhO*W2nDRbu35i;0#fw z0RWo^E#Dxz|o&%sg=K)8#{=Q~4F8SJU zr6&u^0Cu!bo&2Wguir!ba>$~b@MSRJ!^*fpwi55u$Qdp-^aW>iF5;*SM~dFo$+s)4 zU(|>G=jgMMqx(f4xd2q2dH{-5CIJ8=={D_vuvYb;tN0Cl#Q~D~CAn5`^#>8uwa@#9 z_)*Z1lO8$JW%lPHk6t4B%qF65KST5vh;dCBpOcS2g{SaLP zV#^M2Q+^bRS=;fTp8*2Qm2OhOcAy;C6I^40mD=)D^R*&H+X5u`s4#9zOdN0&fYn6? zU*(7bVN}f5cQC`|x)mbtK802GDQG*u32@$X!kJtMPLQ?r6W58_h;7DGuECa~K{7IF zSEH_|!v~3;0WpReeLo6EC3Fq4W|_-^`|(Uao>3l}PbRiVd-RAI^!t<`M03e8Q3%G z0RHyxsOp&fd+Bn<LiU9z0C|$%Z`6sNjs+r$jiYnY|fB#w0 zS9C=R-3K7MLAXrWi62XHfIuZB`uUrkoeO9CGWO3=URTGe*r{jYa&jVd1qL`K!qmQ- zRLJqbBQ9M4plA`GJof$>;Gaqm(+k;s=FNWgA28yX>dN?GZRdqW8?x65I)P_~iDLL= zvfd^XXHs-^bpQlmKe#Lcz>;||%36)BLF_!3QfLpuAQ)(7I+P+f0IWEz;rq+joS zYg)n1m{bQRK%0^jFYy3C1ZSak_tJ;lYg7NF(g_hJ^lgJ7AKw8P6zo33&!l^)1e<{` z-WQqn4%!TThY6($@^7^|$UoMYK(`7TYTUv#t#CJ>8Nn{~*?@QeV2U3)NFRFvWSxUe zBmJKG(2{S`XwkPEFEVF}tKCl%ORYVPEaD2Bs2+14oM;vlaZ`wYqMt~Mj{fQaSp5|X z;c*G^&ZGh{K+#dCFb3f0^KWRKLbd=NY2)<)^&5lLffDfS5#BxKov}Zz-?(379E_E= z=PLZnt7Wh?;%d2kg~%6gh%{<}9fPXDzOT5aLsKqvfA+JGv0L$z$VMo6>w^6@rH~t- z>2_gYU`jm`s7D+Cs7oLMz!cK414!Tjq2Kb`FQT8m9s_@@E01LY`(GH5(M_~ zm99e%wa)3P^h=cTZ2w9q2kFh&s1=NdfCy0*h8c$Yuq>YEkL4VN)m4j;Twp_#t%`p) zlM5jGYC7x+#Onj%n`_252+E-!y4X!X=WLCoPSpb?AoK$A@hh-}SQqGP5_&ij@zsF{ z=@)BE{myTz-Ka|mRUpKhm>`pQ>nYGfe6^piN{v~x94r50mqYRQxyxGmvFGe36{CC> zL?AtXh8f#~+%Kos2$PbpguR604jonHsjJ~C z>1V+sHG`yVHw*x{DA98^mK*T1-61{8JOJ3T$({T^LCcLqbG9T%0gr8FHm7tdY!_X> z8S>4fLfsT?AS?j*+$)n_LD=s1UBX)Jf8xaDM4{F^O3$_3m<@ghIprA0`NJG%D6dB- zc13N3<<+GWXS0@A8vaoBU$@rE{UzyCPQWOaBt~Q6N=-2Gs}KOT!uNyGo=qy`0JsBU zIRO89MNfJad8NOCUxJ?_s1)_%Y8vH=noQL>5V;~nxoyhT-|wwE0s!W}G49Z@ed4#i zBlg)LU7N!Jd_Ntm|9cof7(`JHVnX_MtYpYyBh>WL2AtPBaF|>O0NkUF(pQ43^cJw| zLEyJ>I)ES=8U6tCS!G-D2I#48BR1S@to{*V5?NUMuMX%Tni-Ml{6RpVbIQy~2ukuD z6h8)UN!CYwqg<7kcx5BRH@~}(bIp{1DBci{^#^D-^~BBz*#eBzBfQ_6dNu;&7wl*BaAi?VFnBAS_6{ALN*CIYlc06=9CW7Va&Xb?nKB{u{yVAOml1U8eH z5mbkhzj=F6S3HhT$>$Y3`Mnk)B8NKb=G;%1bfy$EK{3b!P1T|-wB=pB1OO1ktT2e) zq%i@&G;5_2^nC;~E?+G%`Sp(w0kFjqdf}?lq_sQwyC0X>>pL8VJW^e;PlW+sLSY+P zwGnB9Iz_1vAT9oYLSO?xAdn!AL4ZmrI*?))J+lm}R}t`|!tIf;e4p(vPxGYljS|fo+r%R7|oIqx{c| zrxpN8G3vlBswH!Boy$X}Bmh7Sy+@>L|Ku9LeZty}gB$3{)P}2ni{@?{7^x%5!j#9q zd=DTGs1mfoF$n-jdzvhUJZ$}@Zph1SzUnWXk1P=jpis;Aeb67V$*=kk6*=a-H=LQ1+6#xBpJ1lMMX$3{61q%Qrr=(^)Xa>_->|93*|*}oB|skjrg6()G~LON@=vTloevc``rCma2#x*#Po) zRC~hIMrR>(&1u6#>I2wJD72*d9rCf?W{L!FFkr@U0~o)Wxy~#AlyLN|UGC=n4oZG3 zs9GFMtvkZ#2is?*DafFysDa0V97)-Nb`1QRtH_LE8#RBU8Q+_`I)O|Ew6!u2fcgNF z3T>hp)G(0k&+r-=oj!m8;Rpg@)_|-3psPxsAH8x_kpp_dC?vd&a^Hj*wgtM3Lwk!F zaB=C|;aBxHA;{zWsln)V=>AAs2h;QcRDkA0WXIxHzu`GyPQSgY4faW|M^L!1YQWXM zc3qJk1NW{+Nj@=$Hn#;U;>Ub$SY`W8k*3W>&b%4OuoKB{erdtH0j<9b|?sO1OT8IrPHLy9FG0N;_w!)p!yEa zjSc`9-7*J*ClCv=0w{*XQ+l4^-yo%5?lI@N87>NVov&YU@@F9sz|ea|jyMH03sZ4Y zOiQ>e#vEqYC<&xM-|6^3GqI&9g#hsD&r!}E-zX6s0MMzNof}3Wn_dfSWUI~q0G~Jx z+6J^gOw@s0f(<0VAO)2XnC_wnL=kXIQ-*>4*WZ2+4ExWqK@osyi5Q&EltK?kB(#ih z`Sgtv(EtEFsKj z?}S%i)AqVML~?Bb*xld;^s@QE*+Td^q>6b~L|_0k zWkC3_z<~3#Ll6H%0gtF;%Ma=+YT%`?7UQV(sX^pkHbZ`CcaW&-UJ!NkIc6!uy!LNg zFM8@*!A=C=q0oLKD`FRDugyGDlwG;fEU_%Meo0&hCB+q81*Qk$o@!WybCZETf>^QO zXe5KLhJn*U(NiE7XOpC*;0!4H^XCK47j^PA*a7GOT=5B5{H8Y(7JTzQz$u`0gh0q$ z7m5R>6pmX&xeZN}4Y=JA0RRU1Zh%0TNgI)iA4GGm6a6JY0Pjh5($eV! zjFGfK%z`s-66rPoyB9SB?*39d9*G|0t9QZ4VUkt~h^}y3k0JxgW3I&@SH_}%fabl7@&omV zG)JVOlVIuB<0RAe>qLL_5?22bKcV;^f;_5W3jnG?KnT&*N#1Ay zp!ohP^bY1kF)I1@gRx7b@gxVn|E5Dxt~+3GjLS(3WBh?sDLGL7O?WCeI{=Rk1OQq;R>O9#356URw*ohA0Y@1i@P*jc zXaIl~UOYHCgD!kW?R~-n0Jeu}KyX8sK;2LX^a1z~}b`(}UsWjmPV zWe!%sKqEV&H$>Nx+CcicVAGu}m`T<;+1tr_+fsT8O_gBx&cSQbok zbITRA%TszPhx&x*0DxY0^&l$;%m6slWK4ToN=E@2!+i4rb^ z&eFBsVgKcS9|o@kEp0uM{C#u)Kt)-@=D|nX;Xq6a!PD-Kz+REo4@OKp{co5n({CXRif>gWurpkg(-J5#9{y^i%_(WiS!J&L!5{xkx!LG zYMb;u(YI<}CFbt_lD>JfK_VO0~^6D;XfN78vvo!ac*Avj3#85dfe%9*!_MU4s<^*jk{te&Q-v zc>vjMiKhTn@?T95{nYi4`$L@ktO=}vR9kR@7J~6!g&=AQT`zm7qKpMkHfbw6Z3Xli6lOJHg>aXuORb(}6 zT{FFSlm8|}G+|bkD3o1P{&njQbHMH<^v1S*bKsKb#RZ@uaNHw?2+P5e#~d&~JJlg4 zf+IK-96`X_103+bEf;;;Ab1h23YNkqKb<1cf^S6s;~2!lT89pZU^uW2wQ52XKpn!0 zCR1FWATGL|^m4cg(c3jok&b>tA& zDlBL&fb^y9>-_1WZ$2I=VSaYErNW_mv~StASp+MbzIgzW7T`LHm7f%#9)O7+2>`Vk zMoSa#?Ogpocs3(ee>T}T01#&5I0O^2TJ>0FQjR9?JA z^!-C!z>7lu_vnlHuMUOS*BQ!S6AHPaHy*&2G0quM%mV-rUiJBlNT=QafRK8DAh2es z=wSeX4_|bX#D(M+_7_7A!6U{zW{AJ>cqOUzt zWFFl9f{ylCT!V6bGt?2Mh;;2&+}+{)ot)n*w}&_0!f92pZ!r@9P;CwlhdpGp&!D#m zJu*=irZwE8*F|50$ujW;_=Q42NKJJr76wS5p${B<^h1{-V8SLd_P7}jtZi43qoD68 zU(pQ8!bU`Y^12!H36Ub@LVQ>9uR7s!$34}_SA}^QTHy*w&$xer6F6X$s3AAF;cFPb zI*fQ(rEMr8e8}o$0I=spzl~u3+hJ)HwwZ5XZ6KIdt}Kci9*w|ED7627?xuPVel={5 zEkINWCnBVW*|M0h+4u&j5)3L&BUd-0S2CMpB&#t7?hcGx4&HX1v67u7a$rw)8;gc4Q6?zZ z3Ko9@h(xpivbP~HpiVbbWL{5D*7V~ ziRR73mSr}KS+J*mP+tVL=#0-$V$#`(Nd$u|wBbiiFXvbXt_TV~J^fvrzjgtND}In^ zeoNXm?*CV*CThS%=1hLNi@$cS$SR0VVY)<@l!gKPE)Mea@B!Hv$T@Pu%K^Nu6p!RU zf8t6+0>$KWVmQZDy%Y~G!B6Q`#{d%46;{s@#qd^bVB_BMKsWAn<*Gv*54A~Z}^qaxb!+Av(!iIr)t$&>_bfhkAt3lp#x)rbfZES%2svkg(8<= z&z*EJq0FC(U6P*RDV(Eka~uZ{H;Z;3DcMr3a*n$1W0Cf-(Ko4l{0g#I+~mr+Cb72! z16na+&N>BvAc%mZ8n!~vi>4xV8oSBxn9PtsK#_s<_a>|(8v{u=>A5w<$<7?)G*>2T zx@w8&6$@dxh?NQqfRn`%Oq!H6c+$I+a5*;8r%NAEmpy7u6d;C0{zpHNAHX?z{ovI( zea~?Kz+tp^!T97L)GrJdbfoY=QUTNEdDUXCV5^X@?B8rSGO^?RELQijMdhZLSGPO zM9EOOKeihR9;O_K00I5lJ=j}b5_`WH*XN8B)%RSih*(8a&0vRyM+7Y%G)MLhes&ym z(q!^e=~um$sFSY|IR<@YN`V4ZKlOKTXn$(d zY#v!Ef~UUVF8C6h13&-|o@#(#9!34F5rH5Z+W$dUi46Lizt1l1)5c(dqZp7mI)EZ6 zRs;Z$BS3VWFUP@T_SGUE63HL4YlsZJCzmUzXh9&7PAK4|-sB&MiBB=0Y`nx-z^}sj ziZ&DDp$8xbW{X3CA^-q?Cra*sa`2VgT%!}yh{k>R3-7}|^Tklcfh(|_K(sx?$zQdG zsKHl@oPnJi6Jh{*6oJ#1eipVRPFzIqOM5OF07xHhSFKts@;o|}T5Hn^x7mi?3%L_A z9=QSzLR-S60LKWGe=wI;r{5@Y=5W|SaO=a6LZ1l+i(R=06-pC*i%+C29snHWAVdLL zlXz;I3=nb#m zbp-3FE`rrryKdkFFet-vFE)Yz5mbWQ z7J(Kiztp7|8~tJOYg&P;wtMLv-}2PTh;S4QT6tP zd+fj5+;1GDR_PNE@&jPAz~=WlQ`AXUn3MD)wskW<7Cq_~*P+a={;`zFyG|Sc z_!Dx~i;-5E7K7J33r7#g-zFBAGph#89n)O>-ZQQ_ZFvBg9R#KCEwcNJq`PlxQO92@ zLfZ@~V@)ed=ZSs>yATZYQgFYwy?Xi$N!kL8)FVu5r{DF7W0H3FE1Qj%z=o}m!V!VZ zcxM^75d62}wNuEwCVV~hZ8vV#w6`Fs?}8;l?hLt*-NObMSA z@Xv?>4-FOl?L_xQ1tUVhl`!)02{;NAS8q;uDhS?tR#feMM5vB8x%4z z3xEetN|f23icEP+^k=UlTrgbmsJ%AP(ZCd9lG~#{bnV)rApFZwr;6-{#zRij00UV8xse>0y21Ui z=O=ZWnBS{o?t}Z^J&33RDd6 z&5W|i{NmqzA@a@p2*WeWy*rb{1Df?Q3Q3JBZp`-X$Gf3@xwVoCW!yueN8gP=iRwMN%`R$a0~wvy<{%zr=dlo!DZRpgKTLOhA7;^+s#RjdT`ZeotlEx->(a}v+lUw z-}R)b*A$#hCHyf33z{dR?{JO(H?B>P3rN9s22t69U$sUBSlZZwqFjma_!2WY-h#+KrkbM z1Dxz9lxzB#>qTt_%gj9C=~QYqfO9;K=4ce#iioCa69y%N{4=yMW35rIum)hTc7^Em z_)M^%;%YNyEPmqXO9=`*3M;(NI|wFKDqu3#6$OJVsols`-qmmBvhy|Sx&ad?*H?w{ zzDW-Ig|)BP?{f*YaUEjU-6DF@%WmXVJNb*HTp~9F0|0!p#{w_^LG<(REcyc-$S^YA z_leXlRqkP%s?~9iygFmxZpi^Buo;mmH;dwq1H}mEz%?USFi`;iz8#-;LP)1E3)y&z z5L~V4bk9j#sfrVT&&j;Out;Rz`MvYEZ1?YJG1>EomwS;r`6+U}aIIXgkvg@hGfNLrb)7cKyhNJMprd9r3H3h!+J@G|XVd|LtaYzV^%y;`KB43OR zs*U!!sX&2uo)rDy`MHWe?2cI3Ka^gJgV$n1h521ADtcJ5tBXS9PdV!rcdy(1*8ZFB z6!`5Ik(Wn`{^I|f1;Jqt({bC_jDCzW3W|sWh@pnJ{RJy8%~;Pj^%wc{wU`_1W!h3; z+S{UU8|;Q~qkV)U9>%RnY1@1g8Wudw%%! z>yKV?tth>IPp?***qS&Fz$^gpNX9LpvWV!3xYUZ~#6zw(Ge)&nUwclR0$)#n^7l58 zMYCNaAIt71PP@|InIZt-G!(i4st1@Syv{xdR)3ku3D`QUh|OJFQjY@gjgVJvgVJ}5 zYYH98s^9(Y4a2Q1Kp}%AmB1jk3ErS72kxLj3sENk6#5T^oqSdyZM0t-MFILfd^8G{ ze)oy2UgDZVTMi(qE=oZF@JJO41a+FYG3?15RITORVb7*fVE!~WpVxPj-HD&}ej%oH zKwN!N7y$CxLKy*S3_8waRTXu}@gf5*MC2P}5ha&kOY{_2{-eliQ20%L)Aj13=c8Tq zqwg2E=areb3vQ`?<4Pm@^Y(j9)9otb!0)s6^P=IVdVs-zKuQoqa*-0G1VNCH?vj!aq`ReS>3jJ8 z?q6^}_wxakh2697JMYXh&pb0HMo&i-ABP$T004Y-HJAYapn$)k09Y8{!>RAY75ISd zre@{|00dnR-;fk;0vZ6o(REf*($jNt^>X!ea&===S5jhh^Kf-=eq|2;zB5^djz)&t zX))$52l_=6fm%@9y zdY0Gt-8?kFOjLEvv8 zZC;6wHL>8NfV_XaVj&=}0)c0xvl#-pXn^&Ao$V^1#|v1qhir@k@GK-E00NjNGm$}Z z69GmN`)C;8Bn6a>Jb9-Kmu zWfFAf*kRRXa{KBsSo-GTY^i1CQK*nbs45zAsbla|tAKXoBlcU{xs3$Xn>yV48@h4k zE_K^_Q!dg~ePZ``SVr85Pyus{;z!PI*0KBND~mkWsIrTq!1oujkbp$qVhcOeb4_+4 zQ?Is_3jjE7bZwvDz(R#Pg)fiz+#kr@s^+o+a0m5|?f_s3d&Fn->xWz~768CtW4V7N*D%IrN>5%bI^eo=&7q-zhT5 zjexJi_$Psk8{w4=R(3l_Qz$l?LLU~14bx0C#P*#jOFR|@A_B?Ou7*Y&`-rhWnpB@z zGf`Ac(BPdBqp=#@v5W_bMAS2thD518Kq%}cvPo4iIkZY&W&*E9slHg8`mIVH?(gSb zf*A?qxu1S}R8z~uiGAMuUFt+QlOU_qE6}z@i7DLmN`SAYQ?c?f4|bB>KqYe-UTF{Y z_a|kjjWK6-Bb7dA#G%S<*kWitii|e&3TmcWraFJsiWzAii9lzt#j#P}>UXd+ClzZ} zvo%03yC`ji1fxvVcvvQh6G-@p0-}|=#2*pIC<(FGe?U>M{9-glJf&NbmRnl+|lbDS(&dG{|!!xavLUoSMM)``|d)-mkU zg%phQGYsa7KN~?s6P_x5`&?^Kp&wODBI2DPF%(X&u37LcFJ+wCiNfi^xnj$!0Xm;T zuaoxMWNcttbt`QP_0$IsKawoGH`JudLFFpO!iK9Q*b}Lx#$U7a zb`5C^9ZRH&qrN&8yBf)T2{yZp!7%I8>eN;GqMuu7wc6Cfh-*jn!`872y)wEQtRKHYpw^^)wuPCdqS-H{dI+Q@6SA+c-yQ*WtFnZRe;k4m= z5l#`gwv6OiIp5cp1vH-U5Etc?XE!Uz2zvj&=+93X0qubY4MQ5sbrWil) z9%0Pl4O2>Yi@Li?)A9Gt9_cGtbjm9g*reE`Ex%2yAC=Dkqw-#bFwVK>+3KU!*FCR$ z^4T!hnAkM6ceM9247BSDISZw9%nD46pIZD-`IhyKb+YkWV-d8dUMESXG<(vq^ZQY4e{D$} z)%W$k7_;TIuWN&B9BnBa>$=N3_mi5Ff*OL}-^yU(M@h7ZmSljl%Q?&N)3~Z5g=4*E_(^nZjAqyXUz#l6Cz_u5zUDOz zuQPXxW&g3WtWQ5ab?g<-R_u%IJMTx$TYc-u{ir zsqf#1Z%h}P>BEV`&IA#9!CRBRF>N+%VmpKJ*dwAMOd{pt-J(sYM+MVFeMaY$JQrbC zuvFN>Cr3eDi3(l~Q7+l9d@rPD#2Z8!AO9sbiTXY27B1jfuYb#4@}Wr!Uu*u8WHx!m8^+f{GLp@L z;U5z}xGR2T(oU*%+;QA+cr#FDA5nGJ?GjtZPoIL>|MJI&GITRuoLa@gEu*I#Q(Tx; zj`k)FtwS-tBvMPWxrg=!fBc&LAf9=UbH7L2LA?C2wjff|gqO~Aqq*#bM3uC=-z?55 z89I056O1zHpYLk9UjNZ8-7`rm4{m1j*M1pl+5Ri6y$14f7uOhPJtAm#Zfeg$*OcFE zvBu^qGK)r*EaqJ~=`2MWS;$|GIp1}fDA!-Dh`HqIlG3jF=OYed4%-_GyxHaisbAAJ z(g)I+rwM2}7y0*R*1YNBR<^%o))-=}{m0>t{k8h_j|;i-n6KN&)a%z4-*jHP@AaaN zq26M$5Y$f9PyC@9lQA+#nB?A7CFo^nxyw56;l&rjH-4Ylv%=yA^IO}#z+Swd$CsRW z6Bkgnj5*HmhR%_W|Ak`4afZq#!;EWlTk{6pLD|vCody3J)ma9@Oc7}_KaCTke&?C( zu<<0vviZH&9MN&L52YvpY3B znm+nD?Kmx|^>tvu?M~Y+RmE&iM-P2Vzpt>M<>`~_y>_2DAG(A5MMB-RK*kV`d#B6F zGsE9S*GW`KN%v@|=_faK!o0H}-(<8?GtdSAe^vm1hXcUnJ@~i{0Nw%suwxAXlIZ|I z;rh|4TLl2p;Oa01Bj1^SL4JXBCVuB9+4Tc++6_;6U;ZFJctb0N%@ji-1*wv!uI1y2 zNF5{B*q$02d#`x0_i}qm(HWPQHyMRj?`i5=G%O~j>leM(F4L1Pdt-l^J<|{u*GeYu zcrH5 z+E7vh41gS*2#x`~2VNlRpKuzZ=2FX(37bH$i1Y<+FZ{3M_yM0QIg_hKFJWOI9Vj~u z=|2s@1~A~1a7y73Ax{`58AJfx`W=ED;g=cB2E+-SU_rG}qEXWSc*`*9^4F=!$%QgD z)=Mz(e*l-=Zp?lu3d|t<=%6*IPAqxk057mJP7;Cz&xs}#7KV=Av~Kq=Rpp!v6}^w5 z0>%fW6=1i6bGopg!N5?&2#G$w@WoYNOR}^dXx4`_SeI=NfJ{KP(7r?4RG4P~>!2$i zh*7*|{5Xyhzi6h@4%|;H_G;sNwOCR}&u&ONokZc4VeS$nA zT=Hjb;A{RmHAX@+PP@VWxIKjemk-beCW%H%(-A2K)(%~(M+y!LgiEZzH9!meuGsZA z@Yi7^LYHUkTYp;tyt3W+6tm^P>`%6_cKzX0N8tnT8*|CtAvh8NE02?$2BZY|%n-<5puNN|@+gKsMe*l(sF zzS{l~i&*lOOJm(4aF)Rv4mbngt21?${@_KFKp2iH5BW=djlCM4UA}^e-LVp%st!J@ z2m6?~9D-%wr0^e=rvCz9r;Kn0V}lzo4shVP#B8?!oCa~p3-jWKdTUf*rM7>uH8x3sM2NE(gu5Yu`wB=?dIcbo+ijnP{f zr;R+swTFJI5IpnLvzltb!;j^=g`oKa0dUD{K&9!RD$IvuGyu|s;W;~xgBB5qy~HRS z5{hX3I3j}!z6OL3j*CBc{tm$j20MVPZ&ES_+zuy(Put6$%5|yBEL5%@E&gfy?@k*xzxq;rRW zp;|f{acaaD`}S&{FTUmYfB-@PAccxB6JcdAa}dpGwC9Wb6h*i2Y%g^_bml`^fL|_X z&s@;v!7qBFdYR&LU#MICvH=bsb1{zS)wnmt!qDIW;MOM)H-duvTE4q_Y>W$o5Mh-| zABjIrJ>%0JUm&zrQl4*gwq^!mHmaiOGq>0}DmKW(zs^q{|W=YrTl5oj*E|gXP(*XiR-v{!EX9NaY z?&9isGWHGKG1g!x7N70>`y*no{Vi$W@Uj>k4JGikqy^dr#rdIG`kNZ-v*krftY*6stb#pnO8BJ3l;=DFc1mV3 zl`lk%1)9<5SJwf?p&jhh1`vnn6$HxRF{WyKc720URLJh;?@#PlR3z}Ci>OY^tYDf_ zeC`X;vF~rHuPQ#p)HGjTKK$0V7y5*L^_QQ+9XoLUb@%Em$Adef*~ggjY@i>$DVT^7 zFlj1!#A@l#<0RcvRLHiCTzj08nUT9NP1R+f9`kVowE}AMO|EVJv%qVDspFm<>KFKL zbD=5!ZUs*SghRmS4>`QPEnNtIa7_>M$D3y(?aaxL$nPPqe+G{erYv zl%3tCr0TWL0X`Hb=M-y0Z~WRn6L#j>bQ!k_zDDXen&1)Na>I2&=nnoLaBFz4OhKAH zMF(qg*2wN1@WzlmeS}y7&FN+R>TQBnoI!|4Sh9UTP7@3%S&<+f^>S-w24lg}T=#tg z1E8^e$A-9EOuxLp3~W&VFIW#pgW%9c*-$pvo4K;*B@m598vKo8w>P7FH)MPNSGno2 zP~7-9iGbFRf2b$F$nsT4b@6)-*YxD(G(a0f@9s`5U0sQ8ef=6~;@lmK`$waMQ21n| z*S2y*;DlYs9Ult~bj)L!qnE&f28;pKO}o;{^&M|!dS)ALzS3g>v9OkDD$}Ywu7HVc z*;l0{ouxaUzUS9M6zUouy#kSv6y7@J9i&0}KSLsrWWuc~oM%NaFBZVKjJVV$!^_J9 zZ^VLjjY2ibSrY1~b3o5Q?7x0wS#VT4BkAwm`Sh7pYh))qzdm$|2@q%-48dMHX((6i z28jNK{shq8u;1^&PB%eh-59%kH$0>Hlr3H>Zn^(+@IQvepSJWZxhRWP!Vy9F6qT;0 z%uN}Qynkx`&QFBV?0nK&KO$Xx_r5qHTDE(z^*Ln^1BjchI}LnK-fS?ROZlh%!MztOYLTE$X)*JC0v*LR}Hc7 zNdgdv)^=ix1%e+2oN1z<`&z$sTp(%;VCB^90#+)KGYIcJ^}1n|IeYK>L>UtNiB* zkS+_Jxb8OG!Q(7#foIsnXdFMsx)@LnZJ4iknU6i8Jf$RsF{rC7NEcT^0-d;yY92uW zbU#&|ln%pARNoiAz_&Zb{TF*}-?Vv)&q-xzBcp2pU;4xEjsAOmhUw?;!Py9P<^)Q4NtCORl+bb<0x ztImsB@6p-}$v2$FTf3;j4AnflX+g4!<3hh;AOeK$5G-=tJ2_QYhyqMIBKexCZ5qKD zcyi<1Bp~$mi#=~3k*yZayWk~^!<9aw^Y;XHLB^C9H9o6_qC*jLCmGROPYWhkB5iJP z;RzZaa=|1F0T`*@mz!wZ%0TB0`*y#lyF1yEF3N^rVD_lOHYk$uLClz|E2pUwtszy? z4y7L#xldR;2+_;dvgZbD0i*MGRzG-ln>9*W4K)6c3M3#bsp)#|v!%M(i`b%>h<_Ze z_lsZMoL<%H%>5Z_mrX1EFqH;w$rA{k+C0(zJ7`V(%NsI|aJ4hoXSVk+s_=$AR@Fu6DE!3@Ipi3U63 zfYW6vWt5%%o&Sis|Eup5h4wLMJWYqCOX9{;hf^4VPlLXJ#e`N8p0xQY zTJ(|f)+F@Wox8J=N2%VsP>+D)Ape1?GF|9s?<4u|H>7|d@C)zAch1#pvNy%xfQ8wl z<&l;{Pk#yaT}KIvm+cG1N0!SGKIs)_J2&zBL)AqkWYqeWn%E$**u^i+)5NySH);O41x9JCxkPs<%U#1)@Mxr%B?C~z8W*OhR6~c-5Hsj3MTHN#O8io(<0E5PM*K!xM;8<(6gx$nz z$DcUr(tg2Bq)K#(plIns!Uu*9widK}YRbZYS$$Zlwl`xqy$rPas9%4I2e?C$H-6pA zGG9qU|E#mr$z^D)ymZuE`n*8@<$0`SXf6j-o$auKonW~>HXPYKB=wsXYh$&VsbFF= ze{|~!^OaK02>TyYO{_d1d9q6vkcGiQuo?NW+k4r1j}Ug4zs03k2;~_$pdf>7^1LOV zlPSVjd)a-3KTx%>dvRT~hMgRV!iXWit#v0UeE+`CnE}Z4;1%rAPHJ87{0JH$GU-)J z7gy6EYu`cH%O$IsKweSgjh}>^@B!*!QbYMgUor0kL=b>5HR+sjt_)nPtp;wgj3ZAg zQ6GznGG*}-B|q+<=U@2YF6Hv_MgJ7%HP65mcFxE}(`gGO2vL8{FTrZyo_I9=nfW2- zz{uh-1V2eOFgH2zORmnH#@Mgv^6@>Xa4UFnuD>`en=s%XUHEqT z?Yz}fL)beg1p}Op%Zm*lO5{9SS94?ey>9k z;+`KaH|A~egj~50%!+z%qkC6DVmbm_8z_LK+ZI+_WUjepN-4AbP%G}hwNkk81`}dh z-Lrw32L8=Y*n%gm8HJZ2H^Cv#RFErR+@R~h1n|ObEC0HpAjJWHz7bJ!@k7V!obIUr zosQ(W;LzNa?;-z~gEu4C-JAj+O(o$&NBN0Hr|>$lG)5>0VGxe&ek;^NRoH2Oyvu+; zLpEi1QT)H&VzoOIh3?LLXJkpaw(m*9nZ%+l9Kq1}EF;IDS1fI1y9iBso*1C%|`~y5fs78KH{R~>kr2rA)TF>orAa#JZY{<1XYQuV{tq} zXb?tQfQLW*c$XZ56r4Ts`sCOa8!c>1Dd0N}L(2w6^+rqFp^Za}?$p`I*lvU{@CYLp zo}QI;hJs{9X>u5Qz^^T2CR+;#PzM^IQIkC#A}vz5;;t3L7` z59RH%><}rqYTl?*-5^>-I3<9tC_;S(9i@obVoy}cxnGXazqyOm{p{$V@#GFAbZG}S z$%r^4V2d>{e^bOm!j_|;K~(!Xn1_6pr`eJ+8aEdJk35+9iVHgbN%#`0?c#c5c~O^E zq2uGJd8qt{%b!8!hPVr@mru7cQU?srgikCQ${CU@twbVYadVs4@1Lm$vI*bzmarhF zR--s<5X|m4{7r{cC%{(B=ir;^fsrv7bN0YV9r37Aq|K{DD&|%qRL1+H`>7NIxm4jx zK1A0dZ6Nf*>M<_#2Gq;ujkk{a-2+b}*xmVrvNUUK=%7Jz_2@$!25sfmfrv8Bf^GdAW!<(J|ptyWMt za492~?Kk*IMn8G5E}sn#|L(Q)Ck87fY`9u_2QGh8{*4!Ne5>!JgnwW$zvHHfhBt^| z4DVH>>^F$eb7|Q^#Gzmn;|hyp~JJAANS=*Hsq`na;6NwwIXvZ zH2CSeES)3c3(p%Le^&Q~*ZZJ>-q_oFOs=Ei9cJ4D%Nlw0_ZMFQhwAFAjYDu&`7T8_ z(3dF4e*S8hEd+gOBxWn9Ub2@E)Bk9i1T(RkXWOm)g$ijLA>s01Xqwm73I50W4j=h zeuNxCnXiP`a{CBTk>{7f$m8w&s-zriDayYxazB5WdLQ<%1#S!Dl(XeT1%uy($Nkk~D$D3}e zw5Aliv=} zmB#wR$SPYJFc|93Ro;58%Gx^Ip#mYFcm1ac!$42|mU2(d?Ef};a=5ibN8K8XzTFrl z-=Ft4VQ;yKzZ}N;Ip7N0tp;AL2Z>C0k~;|1=XHhjk5C=neSNXNP#^nr(iJ|ins`SM zY}UeJ8j-Ny5c9IRVRa8x0CJNzwR`LHABDl`vaJ-=DCq@Wc{IFEqT24adU6-G{(eLa zO(u#k?_dqPD}Qty^(J%N7V6>z>#f~$+S!TRAm-0o^6-lMdK4gK_5Nu^b`sN6Rhjs8 zU`rHOZldP?=Dr8u2OI_GlCN#d2T!L-qL9mXCsiFpcS5qxlp<~$A{VrhYKi^Wv)ngY zz6H*6zbADpq~3VEHYDNHPTFL-VU_cF`*r)mXTD3+a{lO?cYqrGklJF3Fb48L<_qcf z<_G+DE|{<^nypPgs!xhK0t=sQr*_tGMJ_~FH}2+9hYv&7GCPCna}VZ&DVqWVO4+G! ziq;Fpc-a#X=AZSQI@Xu)viA4x`*2 zPvt-Ym)D@uzXP5;4{2vjx;G7x(>VyiUR4VJZcxHZrHtjz7=6xbUg@&?#P=5BXBviH zR#D!5aL|OUP&ezRG*Ly*o+rN5K+oHIV1Wl;vAAu0AKil0w(gaEZ!$vP3+6n*MAWV&6SXP)l-t)&~<6|b6IWde=^BZ|H)KLy>fVgo+A z=X*VP<2O(hJbG|pZVNoeaECk>zifRMeTRsq6?bk8y2HCG+DtCHLAM6_eQ6yDTjDn9 zkz&IPGI2(E(Yv{()d32P>dA+ zlBNvl>s@OlZ%cx$A>X7V;5};tGa%!75W2O479Rcz(sRVt))(v9i)$Go1<(4?-;{j%>5e-+vI4@S zO{>p0yH971?uMqSJX&sJ2OlUd$wL9Zbh@SsJFNqPhqh}8wc}R!t%CX3kJEGCE>uoq z`Rmg5f-nG07)~`Yl7XW9vGKA9;LT{9?EJQS+{R4`dt3MBRhv|UI18q5=fGfKRQayq ze;54%Ipkx$@?jp!to|^frF)#SACl_u5!;N(h{KgFX~Jjc(J*}=!jZiQYiZ1E*|9IF zQ2*y=C{1~5=&t!wb?1yG2C_bG_+sNWbwg22=Yw}&3BhwX&ty=a3+JvNZhQ*Fk?s@j z-h$-a_cDjl;H9_MaA2Tg>y4OYN_kKwQABBujN}aEI+&$X(syVs-qY`7d>XA;eL5jp zC%^8*ZKmMf@w{hKmtN(YNpC4lmi9V(o{zvuEFjAy!;1swh2@WaFLc(0{9Sq8nE0GH zr0Je2Gb=l}?t4euD;ry1N=L$`xLAr|3SHR{$kOqS)Be87rl=Ir*OC-EOm4j3G@91B zlRtln%e@2V9n2;zWOEMG^}&L-52w;pizSbrt6(89m@!_>NPVEos^vzSfsz3l^Zldm zz{+$9=Z=IIQBaYN8&@KSRE(TC$j0sFC-<6McEygbZsi+-;Z1VuBT>zgx0m>JdHtI{ z)j_}}&%n&y$t6ZNU`lU*x%3MOK%0m9F*fVYWCUcHo6QBegH>LMg^}<}CphlklMmBL zyfu>Mqx*q%EyljC=Q;V$!P;qDQOlc-b=H*-DrK!m@m4$0ov-PSg+94%*|3AuMM6 zSN5_?_07VT?|Z=EF1ma-g!O@EmWJ6-+5oy71+`L~q0Ntlx8`eM8meDy#p>pX3$bv+h1L;NKMV~6@3w4q@?rR&R}s=UVJ(YFrmk z5=Ph$tPsa81&7y-E*q~!z2j@a>N*7QO(@^x!H}O>BK~wj2BsP?a7Yt9b&5%aWxb!v zfm;1oyFCsIbvGjIVij@S8uJ6wQbn3N#W;ijqyargn>pPlgfj5Y*+10SVA|ZOk9cu& z9^Vj0+70g?YQp63mn_D>rVvlvu?f(k39Im|ntzZq?)N1)6+-N>gj2efrhhyRIJ6&( zdv6S5%2}bT!Q!Ih__^Fbqc>h%%Tq^N;q{7OY9*n(5#42dOVT?N0had*+z)3Mix0PA z&%59J(skC+M-+6k6#FVvd4-#0K}0+5{OlN9@F?;((GU9=m=)u~`qAM`xy#OsJFj9* zjnK7e*iSwmo>qq@&z~bM0|op|;# zRaV|kH?*h2r*`msgAhPozoQuEvVed@pT1y176tK9-90 zj3QX+ILcR}j6@=1Jw#aEzIZv>d?G30?bWeS+N$H(H3kFR ziIp4B=)nW^%LW%qw!Ht9F(xaJ-)TAw`NG0UW_0|4?nfohiDjY)#)L0Il5|>|2(ddE z8JlCbZ-m+N$U?|vL!^ZrdV05)66gykTCN!*M{-yOMAmQl+!qA2uOwkMufP|d%K#MkVu@G|HGLa9K`WZ(lW5lWM$DnhxSQ^GhfG>{gz9iBY(>w; zN(T!}NuOu2Zb~d_>SKnStLGbHH@_w=+&si8d3IT3b1FDW(KLvxUD!iR{EtIw3>A37 zN6qf~aCLuc2{9>1v;Gnm)&>*VL<@yqb!*OF5mu~V>CnnE(xr|PT1*-B1s9G3}h6`b-|cbOc*b& zAsCebOF-msRs=##&CSY8+Ehh|UUj4$l~OVQ^y4AK03ptVs06{+Vg(t|YSbYk{2x+C`3Bcvj8(zfTM| zCzwTFWo#@`M;^VgnBsPjFXljJ1SL_CJ%wx9nfeey8 zZPpd!w@rrLbSQ~oQ#3Rd_S^FixYEg%@Av@7wg348Fe@4dp-vvmMTpX6>1D9p8hp`% zA?IJZc7eTy%uL^VgQ>HGcMv@`47d%YBvb}tNhL}wsf|71-zt0LRktF)Vm2aCtE(QQ z?~)-%0aZ43Y-_V+oK}B&>!J0$$VsvNnfllllOmzfYu8+gEu))7tu5~SiY|4AtG#%_ zi(huHMm@v^6==IzW7RsYlJ*l^z$#a4p{%nv-o$%))h!E_>CX zRUqJ1jtSPB4+yne`&u=(;i=dKmrxv~VBq$texUV0jSjD=5^#snM)S+^kjn=hv0=Eznp@R5C0)K`t__<(`NyTMiRU%<-Hkcq)!ZXW2>d-!fAS%%itZ z?Rj6Nl;XAt;4$YN06%+U&R0@n7X9wrPgNY&G`R%~V$eXi>;!FM=l{y+ORz$yif-73 z2k3dYpaU`)nX?~3&?C*AktZhe@Af~~DQETY4$SPITt?#9yn2W7LB^7Bsk|CuYi@f5 zR`nA!2q<6QsODfo+ZqJOL2>kFIpHzBn=<#pr^rIJ=mI zPmU`CThW&cUyW|+(B@4%Q;Z_n17{?9K{<{IJWD)&s0{j&B>T*6g36)Ph+KTST2RE8 zg`nRN^Dw<6eA!dR^J8h*6}oaU*4q~^M*W5e%JwV?>!!Ga3_kSABfGVymi8wJiZbHN zQoZv>Hl>(xUmp(I4CieA)tl>IOwTT{l=55XHjClY#zi3ewx_mu8dck3-i*plF;6eY>9ZQH|1h&L6J89CcSSZU> zPZI2`#h9&(vO&)h3@pcdD8;r%^`}d8^;TGT+E-6L*9XW|O=#E`FWz%F9#I>rD7ACHw%mT}(?E7KVEeT&Zab`=b-Oo*dfS$UrN5%cPn?hhLK*8fHx_#{5K@qTw zCgr1*i`>s@{5>&>oz}}jm!y=Lp3%0d6!bH{ejX|lg}CP+j}02xJVDqq>7~TtJ~Z@3 zMfR`&)zfTUy7`1&j4xZdE|^_Zr?isMcZTfH1ZCDLD?LFyK8hFAQ@Ibx%gG9}AlXYJ z{(BEa+hUVH{|%q&WaBHdNnNm`f8XC3CU$XPQ!o9T`HE=6L+rJt8hatzR>|k!#J?VG zsJXsgss04?Z9fPDW5IHa>d5qTtS>n6Lj=F)f<}RW*kk$Y;KLwVP}35T5z}z4(eELQ zx{Osx-fwm5DLE&D-e%iS+B3~3N1zbJc4s6Tr~(s@M;93O0;r>URJ*<2P&#l$fVA{+ za|nY2TMq>LkOT{0_6-UK%hGr8d$Y@Dm|t{hBNx|u*j-ytYb+hbz*b=0cEjCb9zco- zwkv9MOnv#|2*#IV1S@mt)~2hRs#_B*eYd21FGp3K90KD6>a;7$h$1NHbA5R1i#;ijQh)q_5XQ5fC`2H99q%ZHgqpoVhQyp=4!HGC9VO7omP_q;VTY@|u+w z7Zlo_4ccS!{sqfv+Tb1RrqkO4+v--`u`9H&6gf&*QIXrtwBzB8(xSrsvt{mWil{@R z9s}Q?>@!vnoQFnbTqIM!x`c*stP*Cr7-SbT`#Lc9DfB_x7%HDnf*;g+ARTF^ zFPQk_EkeWQ@E?`*b(ZVuVNwE%kRV@aaA@0|5D|7~8z?;<4%W3s81gqGHJo25Y|LXX zaXZF$eol;u-txigBQGP7~pLY!bV|N$^mfu z3`<292QA?JV1eG|-b=ry7EJ6DT4PB{@;X#aSV6dkBw-9^GIOV(rT)4-xqsKi-E?^F z=a4~9SCGaKga$$(9W@OtV4fPMOLa(|-ZCki23?ZB=5x3kGBAc{7(fK@I<;Z^C_j5# z%-sjNFmwFTNZggQR{xxviM%^eCCtA-R!T@1Jh?$pfT*~J&F(52I#Ni2N!&Xq{NLmB z_RQ1Xsl~9@HmXM8sKvpU_iZr;aM^AFl)HB*-UKiDX|b)?GFe4n%42uRgSE+_B$XLC&J`@ii9nh2V%Wmz6$n1gW1QU9Cb!gquQp4wsQE*zrq1Mi zJZ{L)myUJ(dKdp}M(iEhrl@DSb3PZ+~QL`^WH)Db*3Nh^<3BM7Dl6Prs@~~8ATjY&||8fA~=x9++*C3Ko@P}F% z*dWxAmajxOXZmj|`|nF#jDw&1A+EO@9f#4z?9|#d!EkR8&pM3*AmDyL9ejqH&* z%5b^advHF^qRgq(r#x(ta=4|5FnrVBy5LJ#j=Ub{3xG4Cp=Ri>Z)d6AkHV6bc@)_+ z+??qk_AmmYC(k!@!D$=?eIT=&pHHZ^s`mG=s>4iaO8)IX#X$rY5xdicQZ^fy9+OaUB-a$EnrNwuacwFcdAi-x~&6BGMpd*QS9K45M zu&wPlo*sHm+_^cjd^&I~AS20T&??Vp>XNGho-suWm{9j zaF@gYn%5Oz*-LnlQ-P&9S;eFE{uylKbNEl+JL!)x$R>5LE|X{cdHidH3!JFAl#|{^n2Ghzpi|K}_?BwTEL{0(Zx4 zC-znhQj#(eC#ng#7f8WJcuHWQAaCt>7&)-2FmQ#x)wJ;xt#i4`3O4s=CiKE}VHsu^ zo@(z6b}7ngGrsvgm=+x36Q54;1%7wlZpI9V{37#|I@2HdjOu%S=Q<9uZ9`^q)F-!^$*q58 z@Av6o(SvvYh1n*|@P7BqKSYrf{_k)cqziJ?JNYog>&W)hh|0TQ|Lx_2<=+YP`5=!3 z(S$7Sumd+oDjQMQtFrxjRjy+|?gpa${ux^O0H4|+rst8ZPW8QHr@$oXE}D7u#wQ!f zELipIISzs<0sl1hz5NR_a^@rwkRk%8MY>F^=BGGlVSx!sC8QW5+3m>RjFPn$;b5C% z;6+GiHi9!V3!!LIJmX?$4D6#GR&@L3DDR>O;H@fwYWtg!+vC4jcmLKM2bhr~(O_+h zWPGS?L0J!w1Mipc<%sw?^VI90iQr{pLvHo48*bC*Z@6-;&W$A6w;vD(gVk*E-3=f+ ze+(U+eeL=N^|xJkGj8kfDbjX%$mks(t(4+_imsoZTz9^{M$u^ujI-&mgCf#z^!^uSQ}<0f;f9hm? z9Z^V(O&fgdPV2wo4rA-FuVEs>dPn?(D*`NAe2)SMSC&>2Yzn)2OJiWPn7yj6>XcwE zfShIAg=sP$&?yic6@53h@5ey(6CNUY<)PmCw#d>S%5Lea^Wc$sk`E$8hiKiNvYmNA z|0KcB%Fo!Xo_p_gM9xTs5Bj_C^%5Vhg4Jd|9vRMyy?OMx$EJ z#O^_17BOL^s2n7e&3~9AU2_aQz&a4ku>bAeIe3pjZvNkmS=+eDziP0lLS9D8n*$9w z{)@(CQ&M)e-XF3p9KP`Oz!J`%b0ZHu@I&RkUS_BkK4E3=szR>)-dy?esN-LfcQTVn+er(s>N6+QZXB`R&G0>kw{x@YWX& zB&CYHtv~gTTU1t)Z>L>rXex2VB--;1lqz5Y_X!xB_%N?=17}u4Y)202?rB8H#}Db{ zU3QjyBr*D6H1Zo~JK30OYK;iMHRmARva;;FNmjH}Jv1|td$k7+8v#I|J}G?NogZ4i zr|}pZoMjxZ(vT%2t3o7zkz$46jY8fd(!uN_zx~rGr{(d|19QytKqbwBEWubnGm!3S zgZJ>fo8bSDj%a@svK=>GRKzAoE}rSMBi1SKHFmw_T@N}~8+?WQtK?qBQ(+0@6S5#C zZv6?o1!p;`HYxC5O4qSV0gST3om({Wpp`og-gyV=RK-xJM!NaDjQVyC5dnNf;RmrO z-~VzvYRHbJ{-?pi|K9SEXJYjuJ^-|?B@*RWQ?ru)U9m1V1PwFqpXZRAoRHUa^YiJu zU1f-=!d;zRyC9rG1%tmvOc_FC`CykO6#42`bnq^24SD@3Z19$?_cqrzvwx~Gi@ZGCSPuu{$%92tbkkY&t0@8flZ9>jel7k zTFjpT6-zZbY9iM?k(#9w2gbg{6z~VzyC}d-DDTO4&_pO4v*1dr`I!RRKU1pyv>5Do z1F|eQZJElal8waLV^v#ANT>eSV(y|8aJ6XAa|6xLA(l2boq^L!p(|x10!doGOSZAu zXIiHP;^ey@gc)DRzuMVhaQsw@ePx|Jt6!`QB{a8V4jpvSFYniFr z0%b*HS76s*ihIdD3F;9l;&6w?Jz8o81qtSFBnqH&b5Xs$Y-{_RS4`sOYfJiX@Nb|w zYa{SRifNP$H_l5Ks0QLLV_be?!_s>Md+@0a3n1+$>bqPW`LQZD+ZS_ht_K5~Z78$f z34;!$r%_@u`+9Tm^bxW0Gs&^kP&7_V9`OD+@Ey?~(qkCuNc6t%J{A+)=xVM8oGM@x z@~Ci^M&hB-{>rfwWC7{-2dlU9!Bd{!|A|Y8K~VJoL{2VTYFTg+Ly}iQN~r&;K{(ij zWR{Ciw2?F8%p~ej=7JoffcvQ8E2%*2TMjUL#Nt|Sk1%G;$`=cGqhRDgGj!N8o$h-y zi~yG=1M9vQJRF@`M-hEm(_DZvjp`Gk|9p`Xh&8#Z?a^(3VfVevf+yP_A-2^gCMqib z1T*2o6}bTMzcp=qFb9qdjnsM+#{c5Wk`%5PTv*_Z;^6c~;NBZWM{C+77xGU8PpOmx z6&_t)H`@bx^uyhOxykbJ;z}npSoj-tI&CE->g*dG0DOpS6->1G1oDj**c+!4&z(0f zD}JFf{?GTW_~0hbLxzI`(JIPy+GcJhupcuZ6gxzwza?j#LzW}~RW3!ycb=D+z)(4w zr@wzy_5b1NOB|v6zW<*YM3$1ZQbl`F;O@=b3r#J?C}KdEIkf7f0xz;QaUf{-KWpKNpXjjuxmR^HB)L z6d0a=**E5qlOAaDlBPB0uoD~#T%U{j=n4iG_5Vbmah0U**Rm`Vu=+AaI}4V6%w0KA z9u`1#N7Epq1AZslfo1$EXk-Qbi#QwbT#7}-;+=D&aXGOQFqu-uAL8O2pn?0hJL0MR zc}44y2ae?s^xMNUVkLc?X!^hB7T4j*d-HBSvJs)tt4X6PHeD zh6BT~3@*BmZ_28VhRZCkReH;1OOG_4d$1eBH zd>}%i?M_dGC9m8(Ix|`dx@o&ft$cN<8*+AkBC3paF3>_$}nwh9YFpl z&lc`pv*u~V8&n4de2a0aNoodUTdn<_aHj%>UiGUlT=-5ic_}T2)T)@wSGd!~ks-;kizLCG?nanC6@^`u_2~l7fC7PQ#GyLK?7s(XRv`y7 zzObbcX(ocp}*K?A!Ib|5XtheSq@P}XW zfejWEFQk-%y=zF-;z-vLIbP`(+82OQWKT9Ql)Dc4p0^|Z^ae>Lu$3O(=jI+ief2ie z%3Vlqq#W`iJI}980VqBbn9FDW9a^=k%(HM#WGmN|liXzvUdN}GlpdQL&ETtr+>nKR zO1d-e-M)7WGqbIsMj958A)Pc}U^*@S;%)UC5Y4I5e~{*51YPRJ*?J7+bnVb+A>(9W zyk_S`^UdaQ;}=OVzeRuD{wm7c%;==cG_G9s3HAki0CE2LCP2o8N7?ANJApzXUc)AD zAxPrC(X?{< znNiWQI6T$vD%@_#KY)8T&*hJpOktBX^gm2w7lLj|C^C%p&r{Beq1AkxQk+F!wvI_& zs8wRhtX3eBxrox<8t|VKkfTuMt?n;V6T7e0!}Hx!QV4QN1k@gA`SgP2-uyNa%<5XqpP=Ro4#I7jHJmurTo##8Y&H7t24X8jU4DfoTN@1T{$mSd|W!F zG0Al7{U3F~G@bGb_r?$HP*Hb3fcmI2xl`cR9)9KfMxop~p*YMXda@6wbvQK6!NJ-^ zG}m6wa(xSNRs_&tIxkoi9)r2TI|KKndF6y4I%P&A&0+yW4n5n@yvZVdo#D1)J*1|} z1hNb53=M;q3P(s!XI%(yv5KpE_!g=8%$d^rj@^t_XMD$liS@I;FOt%O-CM3N zhVnsfLZaU)D-yslh$bJe3KH~=fuHBkW5^{7Ca!cK_5kC2{-#9ekSuK8`-r;Qu&D47 z9W*X2c_IGYoU$Pmu<&~#kCyNll5Gl|z;CgxbSkIkQRQYlc3d7z9aiRkLQqfTMM<41 z?nJ$7FL%e*HU8mE9psaN%{g{@gcFK3)nXPVKP4q-%VXCDW0eGG^A?_)^ss_IQSbWP zV)bY|m?!(sy1yG_qvQ6<^^0y^NVoQ)6aab0X+zH&a%8PBnHjqGa%uh>@*XYPe>uWI~NBaIIlRYKr~eqW0W@-SEm*632->FK^Lp?G?}vr9})}ymt2u zV+uvQVZb+px|%)2+H)HQ>AR1##lB|%15N*ue=8>B{qMNiuBXaf-v!j4M3C)m|Ex;Lu)_kChv#dptt`#5kWWo|y zPfnhsp>n|(O3tgvMe?l3BsdFrn43z(GniN`mz}bBAvgBHgTa9TvYyIH5X_z~F2J|P z6--|UrX}BauFY!){K?CzS>sjI@H5Bz_wU28{cp*fWt`6$sTeiep%13OB>r<7Zmpvx7ksPsw!v2<=4 z{QgO(>PI*^Jl=U81k{*VmKId-42T_m*l7(z_}rr!%+7(-xu`k%+N zemkg}C{mi=8M>WKn%McgisKn*r06Gw`}n*LdHndt&z~^V;KOoTVIGQBH&d9V*qca@ zul(Gj~YI@}!=gqcRVIpB(`)7+UL_fsF0i(@oSm_i zg!~*FX#dlOd5tD}2{_GB`@#g?7rpp;l8L4A<*3u=UJWKLgl3Z-zN&3CUEty2DYxK3 zMt65Y75pSjrj{X-DM(+nPTxabjUAsC-8ypdt(E@p?%OasCq`4MAhWng)KZpgGVA~Mf+Rdh6F%R~51-m4*q8T1 zk1pi|P;S@zVk}7h$c>#zT>u|i zk*QXyhm^@r;1!yu>)z`k>$|PKycU6e!+gJa@HuxyaQ`s?`?C%OBzwOV!b`8kT!{y+ zF5p#ufHn>39UNrpe=A2hAd#qrDyj5_L7M$rzFE~YobF~8DcmZ^f1k&_3BNHUPf}+* zsBEfv|7hE%k~Fsy1_FW8ZEI`mqDM>+#M>wYxhpoPMnLNK|3iFPDzESdIVGK)kzBlZ z3=Vy<{L!h?UK@6w=EBVP-k-#tlzKzKDad@DFFh1WyrLEp&F+pTV3gsRP7$-$!cQILO zWyDFa(6)j>!ZvN=-*FhH&_3U6bVwq+;`KwXY8*f6{^EN`4J&uWlBNg6R7hUpKlUMo=I!T$p5qAR0ZkgNHJu2jS4k*XLYN(e{Zy?XZvr*YM9BTDGKxJQDcY~&c~rx)AO3}l2L_<6N|DG_(K z@8f$_iCFaA;OU{uIhpb#{>$Z10RP5oNvrFf!iL>FS}+Ld3NW#CvFrvb6icIg0t$G; z*2QEjg4-sMt32f`&CKiOE#$5Q5LlS;G0ly*?hj-{R0<;!tHrCxpDV^uu_qtvw!|hq zPX`w9Uewi;>ulWwb25c8jxR;?Gme8W8oD7YxyANT}!DLu?+sd>b{7;906&I zU`P)`zi-sfIVqW`CtMM?{V!GX|5^5xVQ)DZ3+Durt^X1yI}6?!;kBJl%E3g_`C|F( zp#{9{r!dr@=I8|oidvVV-wV28zH{jcp6nY^yx2&TW&F2-SgJ{lZggLDUOSM(Dx5;dYFj2#{Hj3 zs=wDtD+Hi_J0Nz0B1>q?^1J-86>f;Vd_#Z?b%?X#*qi7VYpq(RjXO0mgAct))dobtr ze~*5Ug-qw%FKvNd_2h$uaWd9I@Y&_R6_NJ@SlIaF$T;I{8tDet6#pw%ne4==yo6}F zrFcuHkuU2I8omzT4WO+q~wj1qtq)>79$WPJyWp$Zzh*dAS;7wS$}zmu=6yJLd{X=HQhL3t7aQkJxZ573x6i|TRVlL9 z%xarKrz@Fk4kkumbz1D9F<{x>n~1bF6y$Ypm=Ep5PLQYWXcabHgn|PL#Br{TEM{(; z_f#@rUDRBr^1S)_#^)6g%7+?3vot6uNi&<|_^>0NIkb86YPA-{gPm?f4BRfGz@Xf0*OXZ=iPX1(ZY9yOoJl(u~eQZ`sBT*bpA%rt{u>pBK8>ZbSZC)W0_ zzs>Ap4Y1AyF_r$X_uip}60Zx%2iR$!=o9@R%~fdO6_2X5^;NeFnhZ?GY@f(~h#9s%kFW`l#4|>)Pnp6|)-w1nk%(M0o`G-Embk29~ zDqLV~qwtz2BR85K;$7a;ybN5RYAN{_Jypw`7B%^;!Jj|r`W9akd3{?@{+Bm?|K*dY ztpXgn*Z7Q7U9Tg1m( z^RT=(2q6;vYLl+ySL9c}oSb7Kr_-#UlJv2%LQYH-yCzQ zF<1@Yz(oEiE#!@Y`TR zfwR3%?TPcM3oyoa8XMdD`IS8XC9zWXnt-J0E8TFh8n3)h&%ztI>Q(y3*g)x98{pH( zb$EL}Ku!$XwM<y=l3g63RYRVnVu`b=})%`Zd}eSestT&$Q1MRm=&~tp-9vQ)OF;$1XEeX znY~=y5&W;eg3jx6`dS;pU#+Zw=+nsG3($vkt(!pIq8lO3nMnV3{`AMfQp`4<-oj$U zAS`TtmreW*iG`bkD(c5*bBsiv|IikOgUV3kp5SpRV>Zpz+4fO+QgXP*4rN}Vg>g1X zOX!u~DdolUkn5Ur&ZLyoy^$m|k^3+5auUhM zRa{^%4>U%U^U|%9oL!N}{+%+!Njv<(uBjTEcM)Sr+0g|6!;GWH1s#}QvZmob0J5O3 z1u_km2c>PdG!0Ts)lcB8JpHq>Be9g_v~&e_;f4n3xvw!-AE~|jg;wuwv=Z91LZ8kn zS~b4&-M7*GNpX9%e#R;OgtHJ!ee<8dhA38!qMRKoat0bZf^JtFowk4}3-8Qne0>4Y5| z9r;rx+Nk#kUqi~=Xltk#;PPe=SA?bO;QHCI7R3RH9N{!R3uUqyCUHGD)wuZ;PcR%X zbD)Vo1$zOXq1y5Z(KoWJEve4#!kvEZR^R-NO~F~WO-yHH`u^-4dv{;{iwBnIMZ6hWnw9A3sKPfYbzNQiunPFH=Y6rFv&esleZ4Dc2PY ze+s>iVzRIpGnoEy47yX`u>m*wr^7r%n-D_iwUd34o90_;2Wkh-D&}ldoyhmJ~Pdy740Hq_U zl0+z<)Uvm(=nmk0!1dUBe{-_y#XSg;r$BCK$&b6OI*N17nl3#3!ncbjyi(#QH~rAR z8TaE@afJlm7OKr`ka~1m7F%JaL=cJtM~=DCxtEIj?Ex7qjJ)zc!$Wl`wA0AGSoAhk!WZZlVro6OH~<{R`>zA`C+ zCwz-v*OCuUjU6_P>=~e_u)>=so;U5IgkK42R&>reD})`9kQ@LF{J%gC=o;IwzFP>- z;A@57Zy&eJdI|EP-jjOrm@4Ar5hV%SGeZ&tZ09JH53~=<3u@T43SaX33-%1;Kbz%lGNg82lRX#T8GJwV-k}n! z{JEl{_KEuNt4RfpN!XmOeO7AYtMs(?P7e(ar%z{ViDM8b=1@1zXRY}J68|FJcI8l@ z=kezsZ#+eBKv?L{?`kM;aQID4XtrY)9|#-BW=_%6%(mH#CIuw`@co-~Oti+$yAuoe zo8Q(HIe-;huN{9gu%+i)|FNLP=5YFxEJSf$=`!&7nCUtf#s*Z8$|>rn$yzA(@*pzc zeN4#C^_nW7dy%q}luzu~iONs|CUtB7!sME?N^0Ow^TJo8w$?tVRKp_1SeIF66f(2q^F&2uG@*JALG5i5wA@~G?8!Vo}XP(wk!OM ziJhbLj&oen6%gW%`tCp`v-C7IUoL&HPw!mL?; zhKi)i=ad8)Qun#9x%7>t5agA58-1;S$+NO%fvJ3X3Ycsf7X51{v&ikv)#_CcW%_gO zUPqhK`g$6ihr-ba+E(p=@ll4dNc8&dC-Zw>OBP7j0Bzvdt?EblVQ}{^u+pw4QZ3MbarZhc0or5XTt<3z{ny9sd$i%#oSHe>4D>s#6#QGC>RAmn^iSndJIUI>|HG8_}Kb_apec6TUdZwpA zC|?_cP@kErZkq6IW$x^2q~f0FM}s7TYrtqXbbZ@&l&<1DpjqXnwtU_d7adk+oJ!w3 z<&v@VWp#BGW&jCw?0>pzK695l-&9f}Ih6)!e(fu5aO$|uY=*1`j!+fW;`_yo?FK!q zpfFDl&N5{mPgtI+i=6P#`_iJM<7;4CcRO28v=Q=2I9WAo=We3E1D}jmF5n%ye9M@S zmlgq(%b&B?aBq(&N?Z9WEc=0(ikw{_CAijw3Ch8+Ud`tio$760dq-z1>6iy4ki(oh zp0`)i3LbgFl*)>BR&A+YXYY;N>nA-TcZx&BW!~Cs0zGcPH^{z8|_4<$x zu#Slx50r~<-SFODvh#*o8;oRj!OSz3x}SY}GMuRR7&Desjs5GE%VMMTZKtDChWFYw z5H{=j)TL{(KY#pA`taQ6uab9&Jk?0I(*yOGOOyn`+kwXCVX1P(Tj4wSf)5`ML4Y;T zc!8pc<#PaiXB_kfv`7yPu72HF zR}XBEKvXR2^98)%QjGrekLac08j5;9S5g_%cIs&OcDfSUt1jjFVaUMvc+Y;S=+=80 z&Yr$!EImZ+a!1L|t&i^C9}{c{a^)i-2!qjy4nI*~@-Naks`<|ittgHqrMPL0QUGxq zhi{B|1vS|$o(x9I*i05a+tUI7hTi#-4Gj)<;Vl1n8vE1v2Y5y}UH$&hSYAv?(ac!g zW|C*xw5G`jcp0dPIzuE`-A|7=U2>@V6R1^qV<}>?1FD0!xE>WmgN~smUX2RH(;V3e zwS1`03u+$ml-84(^i)w|DWW|F-~O&D`nmrmukCyj(xnu;xfj#~Jot@sGWYM9Gf7QJqnqcwBP4K2vtwVtP<3oS^teI7Wx~@?f8WCxTlw()FCy>@)nl6_E=b4 z$}|2`pA4l;t^f>=5f<64p@Q|i* z!;G1P+dF9T!Azj?`iYNco(o&oEb?MVubop=5-vg>E{`JYzt)xSTd6B3odmOno0oSG z=5{H7f(!!9H$oVtH(I_zwQR;i_Fwa>3_Gt}%}xz#DGmw8_ngES6Qi@%17(kowaH9V zcdM$ZFzb_zfw{l0kKLc#+GT?b156<~H#Xk&Arylh#de-PGyeEVgd!RRLljdpTvOFv zlP&TzGcrhqk4RMK)}sVF{Ux%BB`E2QM+W;zx-Dp9ToNG2uqTOrOG|j%)h*|14EUV; zSuU}^pdbEW^sNs9#!uHx2KnD&)VjzJ^qKFO+e4Lm2P5f~(PALoUtS;6Px?#a^Q6d& zZcMh(r}5{jH!9gHGNrOsG@h~@_bMzZO2aYhn2i$$d^_^ItZ2138}|@i<-;wm8SxXLxW21E^S}uTrEL9decJhfKiybF9>W=U(((85 zZ1!0caj!yz9G%aV|9_CMCDg|Pcg^1vm=AQ+J4<@QF49? z(IjEfm&y~{iZ5n}fn(>cj#SNS#9}k-Xuc!_Mm*_}8d>^q+kpuoEvxznDv&U_tc34y zugVk_oq1Q~c2y#^xP<);7|N;&VJyPdxb~O+$+<-yVeAz$u{*iA9U}C8T`9h-=;7cI zp=MlE$5$X(E6kJ@xqc5REOX`Orp-W`VxO^(5M*pxj;Ws?j?Nm>k_O-JfwtwL6W-tLCkq@y!v za*?3=-jSA+LRV(a@`0N3F!=G~!@TB@{a3NDgv%{7Gx^r1%fHQ|k{7b>2f%)`kz&8r z{lYv^rc$)cy#eBzkbeVWgF=|*25D` zI&jHW5Jf+zrvmNEbuAx06bCCf>wIub6lTJaTMo*;D_aBeb9>W3HB-otLTvga#f+Tg z@N7(;q8ZAB(hfzZnPM@M{GIGlQPHV$JE2VmqTr*yVzB2vc&ZEl7QhM z+yQed^%Vi&IqFW4xfm5iSi`7aAZ+?Wm_#Hx!JXoto#G7I#}#+ zkDTZkpL(M2)>JbPq**Gw{+wY z%NFPh9<`JxrqSiSpr6(DG=7xuLX%MXg>7-Ojod<})zyo>Y`tc;IiDXYn8FaTp9&jT z{>DC3C;`-5q+1+Y)Xw64gp|4`wA|nU#CNxJ$%9YS-|XXSzeYe;RMdKQ)IOFLJEIEg zIYopv0fC}m+oDBpP|%0~@vD zb_@eVi!<=ZdwV&g93vwmQUHkhRp+7pkI=gmM;SwHeD<(z-N$Pla;Evd=A87jZPHQj$pq9c+}g|O+>{jb z;&sIBQFS@^w66V~vZaoHmsAuNdZx88EjSMuBxR5t*AQKcUv(0JFSZ=Q2I;6=;rk_7>8eom-TT*2e$V{&!ZhljdwqVr@cEb zSD{1-Q+f^4!1&5`B_qR+W_RoNyv7&JA<$H_9w?L=+t#zNlJe|X7K*$f;ld2gU_Yww zJ!VVC_8NwVp4MO_s?V!&@{GocoR#wzCzkKr7}b4)x*D4njPZ8I3Em+Q`3jT4o~&RN-FrR_Z&U@-}Z9r5`z!n__fOYKlld83jvilNp9)Bh@fT|G(N1pFh^wgt3Josv-UOkyyKr z7K**hUuKMz3J4U0zFS7rwbHeYYbh;%bkpN0o_^8LT3PwX5c-Z7nf@+-n-jjB@`~=R zT8A|y(osCuG+cu!s_6IDeyVXc;`WB{*g=%0x1|Q_!-s3alkXIT{m(NE*L?P#&w^E6 z!%U|i@jGObxP4j634~~-iz6kA%+el3L9DG4mIk!IcfxWSSJ@1(L*gfBd?;+wbg-fj zviB7P>{4nkkgNKr!W%y2#f-u8xXQ^m8RqyG9iMzA*b)GcX6L*+niL*A__pZt&iD8> zVAy-G@3G7H!Qqj2?$+2!w)Z!K5s`<)rAd`ynU;ov6nWRv=2p;XJ(TcVo}@i38KKQP z)>tE04EU%r#NgIG$+WQV1L`9LwNAq;tR23*a}DzCe7plCQAz0BGCs5S26G3rHaKLZ z?yHQ1fkKAADOM|OiQ$PIm9I*)NANVPZjzn+))l<6`}gI@j~^kxUY}Baj6jm4w*tWkpR3wISHF(B z6*%brik}`Dl2+w4ZVDzQJr#gk9Ea~$`_s9&ZCx|u<2f#`p9HX?tIr`cjte z9jtl@)+?}k!%BWpBy0gbY^(Ft*eQvlrx#9)7|2bNTCmw6{V8)+tEw>((Jk%0ov=rr zEKjd4N%j%qa36lKrSrCEA#p0mSR%M20Z7@^Pkt;aYIz^KZsOzn2j92F+R{66a$W2E=4E?djDt#7@nN@n1#Ij1KO(1h-H^K6)dRhX|MWuZnOYM0cA?>04ecQcz^pMs z#2iMJeA9tt9yw`d6kNG-^t&obrhJqCH?=UBamS@*tn6eg<-h9x%T!kA`;;-Gr{_b;#d-#8uDaZM&#*xIFkJR@W5Bn3#ryZbm=`J1S}!OLMCkV=sDen7zZ+ z-iFOv*eIR#ogiCRsR1J8&GSX7BZoRAu4Rs7#L88rn-ioOkaa1LP^cFQHp1{cP=*UJ zl1IZq^?p1VN2fE9dn9(^*I1BCx%m741&UNm!$pKZ=omNvXb-*brep0*0jo@cpNUdk z*r$8}j#XP6b6s#V#YJs{_4bSseaXQj#xKQdgTU zUT>JdoNMCL8w@Z$G_Kw%$Xz?c1ON-66ddz+dN%(xF``An+kj9q5X8NsA^VhSYki8V zaKFQGHZQ#(cf7)|-YUQU54u=Uel^oK(Ew7e&6MXkEU z=s-;}8_>Ae*r%_Hq4cmGh_EQfB(8x-0My;uQA8^%$KCK_esKhqa@FLGUphmQp+@r- z@(S|$+rY&yFYw8{F!QaaFDmK}Qu6A|ADmqe-|f-(Bu5|$^lL&t1}C~dEek|Z9+?i2 zv<7}m)w4_b)5WfGdwEaf^bu>{Qbh3M*vtEg%JfDtbq=sz(%qz;{k1c7E ztb3WD#jAUb0r4qh-=lbtr~kDn^oy?*TuOn!CO!MJx7oMgTMv%#&uxQuG+=DygO|K+ z#&SNDCmG|k^GhVb!Cb63S=kTLNxI`Nphl4Uh488%(WFyUQnN-GzIe!X(T2B20E`Sc z{8bIj39Ru;(DQZ70vyT~L(EskjDq9bOH2KgBG9h`0c1E&gr)^SrKJk>?q&qUA1uy^ zbaPwFbG6XruP-e#oZN*WYQ~=5Ize#Axn3&>aN$6`4D6F_gZGa)MIO}$M&h-f11q>3 z(S}?$UAS7)(>uRHDjCTe{2t^CDUpUeS&WPY@r3M3i4i!GiLYwYeT$ud8F_n#^^nq3Gr5bo^&VcM2foTk2*s`_#0~haYx8fE|RUyL%a(1`R>_m&;T% zMsB-kOYQF9k3oTVfj$1i!6qgEG^*?;sEBLv0-95~;&*&{l?v$vo2NUF-!E7lOJ0iY zy{?Vwjfsg-nraO69Hs3p^LaetXB~e&Y6lA`*4}IT!d2?L0x~rKZu1;xtUw!nOj|O% zY>-nKoEA$~7>*eqdDgcIb!KA-ExWoDqE|FVYP;uQUbp5iUSl}bzn^6<(!R0hH4-Jb zeFT>|``YsFv0y`a2|A~v5+SAX(md&YHc7_Hk}|cYuh1dC@+lOok`hQfY690@8obzY zU&|=ph~Tb7IHB@m=wsA2&qk{1B@bkon@}r{6&>k8ly4K#CbZt=OjgicLW`bS~l!H{+Q~S zD)UBLPa%yR^EB37r}XnTEQSw*xS*JqbT|)6MTqJ3wyXmF~+$Js4+(rMYn$dh3cN9oIdE${9?9bBR1(7o*3F2r{g9nDf;b^Idd`8S_V(}a+K$G5$!s8 z7}+U6lR9HZx>}H-gemTLn`q z-+I94nsGgtnA|D{xa)h&-Tv8#>!I#pR|!w-DEJ{-k@1pi^$q(^qrIH9DTmftv?D%> zhlR(!YVYmsgzZ_%V3D8r+|}c>A0GBwpMON-K!biYTbpx4vwXm?}@= zdh!M!7mmPtv>)+ks)yO<2ds550Gtiw8GeBT%)kifSq22iy(0Wu@rvjf%I49R$KXY3!w^U&kw;C7d zj@w&p;cmF%Q#<;Lx)L$G!Fm&%F%#~OP7nW0B-_WjfOazr-|IpdmlpFdq)SxBqEV;# zeo^GQUYzg<3=l%nPMxhaJlJ^^=XK8UEf%rjtg*DdzP>dPjC~t*9^$42Jh~|Dd>|~8 zIrjbgKes;M5v-(5t){a2<*>}anfN`0zB@hL2M2e9=s!N_k$M%Ur!Vh;RBp=U$An!< zB5JziEp#<8$)>j6jwN=qO$*Ip5&PhlT~d4gp0I+`CPS)gj*uJw#@96MuKP~cYXr=E z7aLbMyu$T2McJH^+vo@yxR2K-hrDG|Vf_27YuzG5*Y^GIn#x|6E zK9bxoDec?PUs=Opw+=@nYV{|guZSshu;7}8o3_K=Pv2_VEwwrf6Ydl~94=;~1d~t3rDn#v=kuOQ|TUnFA=J0sS(o z^V*e%iY#Gn;^N}A^|*dH4&i=o8?&mwM&HZ$?PZS|3xqJzxK{4HXJbl=KAAgOt zjjBZ7N;RT39|iP@HCt_~Sf9GMeCeTL$~-G(y=iR3=cin?-wm@3@8UI*>D_ZWzKeup z*FDOev890VN>bC!guB-}AwoF!-Ire-&D_H4Q-L&L9iG?+JYC&4Ffec5B~+HIn!pQ6RA zm|Rq>bLRmU{#>e8;!*{7cx>xPPyPu^J=?x0ZJOdwKXZS%A^c^!((JvY@-o*TQp)88 z6N}AvACvR#ENSYCRb^uH`vuN6tG)ZdjZx~ku|5|R-jRZ}?Kfu`-AqZ{lBEZcslCB2 z`N4i?FC|yvcO~QQCNoiRmopGsBAJXLnd4`4lO{|JwdiDmOc@%cxf%v$r~95C`nEtdjMTyM#}p7l4$_$E&1%zVxK}X6^qev8ru9hVlJbvFN3osr$KZx2 z2M{|JYkSd#Irm=E*s%jqh5|A;+gn-e{$c-Y70;PPo$L2|RNvQf#H^&h&N<+oDgS<$ z_+q~0+Kez_#EP#{DY~Mx;LO&fpU&;#5&lVzb33iAt&N+_`&0Zvv>g=sihRo83XDCj z*dH}&{?2ZclM{7Ph@-dsk@_M0!9}VxpTu=*a)h&$)p?#Q2}GCt4(7{V%R~rQB8XDf z=hc{*7pOT&LN5e1%fA&5fE7vfStdqPfwKV;+nMKRlNeun^^1_J+opDbYdEHGPlAik zR&d$nM_6zpr6Ki~8kV|Sd2@Zv!X-@iny4XWSISf){G5+a$NG;DWBSqQZ|9GUtl2>B zDjR@MlW;X3E(iSMxEgnh5Rl_b(~!8hF(|PS4EgK6Jz560Z1|DHH5aO)NOS8*h6^{< z_k{DqlUxuQn<@^ZOV(};X20K-UJeR#d;VZwL+w&*NBC@tr3aqC#~KH8!WqFNYP@R( z2a5l@LC^7~7Gc;+@Yn%+J3mw_8s`sJRdL>(;KtYji%1hlh-h%RhCv^+VE%}%6}v6> zrv!9v#z)&_EIox<%({H-)cGMCkgPn*huZXBC9ojTirexw=Pz_`#aXgAJwB>3g*zR3k^ zTzKkkpXCuM7h*C-=eg_>E(hOsm>&Z^TRW1rfsgjmbEA|p9g%&E0PnON@NdpXks=it zSz9mBwsSfWD-BmSbZvTo3dN3a=h?PyU!^Kn`z2N3*NLMMdOlo7tmaBD);*`a&W$Qb zk2EZ&U9P4(7>%>cGug>C4MBj;9NVU#ox_l==KcHM9u(C<-K-5|;^&U}kX5=`R^@Ky z&l@e}c5^gJd7q7qe`lm-k;ibCC@CUW;-0t5C=cB)4dgCG(!j#W-RztDL2w0Yn0 zIRhWyuE(QyiHp>tkdP$&b~>IlU1o71V%J&fQG1G)JCUPI`&LCMMHv#!$xnJ=0=*C| z&(Oc;5f{M^p?E!=s&|D!a}xdt=PE?#jVj%4oOm@g-4V-5`nl+%a7}zNp9t1q5hzFK z#Vo(mwHW|6!vVd1>h_6%lgbQ6zbOE_Y`R)FsZe)^&k4R*1!#R&7JCU3pt#F6oZ2}3{#Bhx$TxzISi|+z7H7zG`&zp&{mi!SIsAA2QTVD z9yS9nbJi!8mydZ-L3VaC&7MK+Y%O*n1SX1HpBWs!-#q0n4C3VxT(-QJx#Dh}xogSm zbwxz~*2mIht8j$wD*11lSruSU&1kM}oJQU*!QBSEfX8BqK+$uhZ*5pE3~@qdMoe#| zCMk(Rm11z>^K_+^&$2tiujqo@eM9Wo>&7F0G)UFKVOVMU?l1WbVmLxi=av$Bmq%{j zPCCe7mk_IqG7Ek5ZnLv-#52+oz_f64&vcJgp)LA&Z(NWnWsAofW_97G%e@hajS$Gg zclY9aN=!UQDC|gs=1pQ7FQYi?VUNYL)nM6!_51kT&!%a_)@0yx*!NGOU^Y0J5RK6Zz>YcoLUDuI@nq$s$8D8efSquN4j^cI04lR`%-`h<<(QC-_LiO##I_1o5=(}$lyCavLpSyF ztACq*m#MGt>5c=xV_Z(q)`5BP(`{LxDb^y4b*DMbo;gDd)6wZOby%|Kn5SoLU9~^| z?Rm^{`CImk$Ii~%l)?&eD{4)g#~oljn!_~v%UW*37Jd8z4)HPTIkh2-{~2#lC7P>E zFTmrgPMq82W{?v|TnzhKS#ku7SE_x-+t3PPj@J{xA!G$VRZ|u(2L?2~i15%;xu&@o zVDPayc_JKP&%I!^;F=net#qkLNdwFrVImD4ZJh@O!ha_Ct4@_mGIaWYzdadPRZq8)X;{-j-AK*n z`Kg*Re#Om40I9in^s<|9?w%pzzA|haV6Fr3U1T%v(5bANitg+oF->D<5 zF8%_uQYuz8ra=kv2 z41p-?`Dp7~jO8L)WdqqJL=ZXSb?XAH2OU>xdauPKN5-|`Pvdc=L3^?furQCg7%_C3s;2g}+zfDaE8M-`%pd3F zA6cOS#jLup^d0MHO)FF4GW~eAjZ1eu! zxTo;lxIcJNJ+N&xPioTjXasyV;Rcg1gY}XZ|Y~ zwB{!0s6)J#Ck?U38-kX}B_dFydM;gVsduq3Yl1_3I!E)GqW*Fh+Um+F4R z07}izh}lg82h!`bKQxb<&wM9>+up_-rR+_uALs(W5>Ze%?%m$rW<#ug2!B{&_RR;3 zaa9_?1guAtQnXM+judaoUXq)}oZIPV%b2oh9m_UL#EzOVy1)F7p= z|0@^1NQC^MST@@Qf#~XnTynJsmAk3Q=n_JY3rGo8{Wzu?z2$}v$z&|?9}El0Kmo8n z6@Iiq)*%%Y3+McPQ87oDV#YKbKM+f}|H|SZJe3CAF7>#fygBwBEaD>zoa{Kf3n#qG zylfbbLZa`Z5daV|$Nnb91LYp$pdb=)-gnMey3#IUWBc+AqF$92Nw^53ow)@nr0b^G zA4zQhv4hP7$#-khxzR#~+5keB$MQA&E5#>pT%0eINPp-FIm<>zQ z`ic`yMcq|9JQNpZ6R1Q?ZE8#HbxljA=C7CYM#^Q5dz-p-8go?ZjFRl++0ocxOHaYo zF5m`mjoh^cikT5`JI1}ktKVb3)>eV}mW8NEm$N2|O7Rn@E5C(P~_ugfGTTS+4(E5BL`>jvL%&_Z+lq zcf!o-W%Y`cqLrHMr9B?GbRY4l)mu^3IOC|v$TMw7X(<`z(f+Q+iB!ih-x4^*;MGppX^m*s(xNs}mTl z^m@SR$36`F)Vd^KOrE8EQ(_{5=7PxRkb{+w4>Rb>0OY4eQwNK`<@Q0^t>%GtF}Gkv zJwc&CUPFW2Y?{6U-kc6y0CH@|4nHcGH%u*+Gy??AZ6CHhd>zH{mnEMHW9nn5X&feO zcqI0iqL!tn)Lt1oEMpe$9w&!RQ@t|MMK$-$MGHuE6Vr7L>ZPN4IzeQIWhm-LJ6o zCFSOlgoR&YLG|+b`)Vu3cW;Sv>vtO`w$T805lkRurG`jf$qYS2x`3r<;o1z>aKM{) z&AWFg9$DE~8Vh}9ARJn9QDm>;a6S-W8v#LoVkDVM>uhOvzna?JP~{52f&!5hC&O;w z)j2b#;mSaAZCmjRT9#fpN5~#{>Oi#lJlAZYrfBISFDL>>*RLMuRVA=Vr$6&U7GDx$ zs}RstkpQhc)x18XNN2Mxx{kDa&QHXEQ(cxEGNK}v<}F<=C;wDfCtzEJC)QDV$x*CF zRVu#waM#NDOOJFvL)}5w?}jhQDNu^Hb|A}r-(h^~ub7F|hr@ylRwmU`HptIQmkdFu zFH@Qq5Uzjwl=q&u~oHiX$Wr|L}q+!&a5l#*$9^% zFLn9Xa;kmL3Yuf85e z_dozC2ua{}z-wg^uMOfpJAALp#HVXP+G>C7H`yvP81E)bS7TV_jVsD6sSC`V#{m27b2i(j=G+KheWeK+&=d9nvuPvFmMZl?M7 zWr4e!NuO^ynxiu^5Lh9jT2Tc|jf;tu-&ut_9+uD7<=5dEB{u4UDd$^SasR3=K(;C!S}1;sJ954-N=+j zm)r(vmKGbZ9e>qj0XdseYpKxrt$U>s#}=bAqAkjO{~{vC-N(hSGjU6<72QqfA+=bj zDuS?gr}n4-X@h*ceMXRn2e%%)KD7SiVMh)UN&WaN_k-v1D>mYJWVYp`p6gWpwqcN~Go=5$@{?X0|<9>Ew_Sk55Pax_$LA10#kJIhejYccU!=F!dUM?CoAcr_QPMU+R z{96EjyiS%Gj@}QeWvf6PnKn|Av=xY4+DtGJ;qWAXDau`i*2>ckAewp{-Naluo*H!?O3wH8~OtsD-x`NFjMY7q@u zRCTJR3lvZ^pKQKGj3#I6`rX(xs(eH=-pvZXVK`llQ?ZhwwRrl^@}^wE4R0s&oVw*5 z36w(uNzkKuDdo}H#^~1!a9Iw)8Rq;-Y9~&tH9`+fKUo2+pM+-8GB2Wf+p*KFS5!Ia z9in^nkig7bBM24W2R>0;yV@M9Zj&ufZBdN)56zf;txMamDu759C-o)0_wk8^Tx$O% zfw+>{jyL(C#}BEgC@TKR+@7yp_^!uq0}g{;ON^VU^mcui=26VaEcV%^D@l8^9i9QU zg={HfKGX@3t1On2tgX0A!iCy@8q?H6UvR8JDZ~J%4~UiY;XBQL+-6Fdq*o^}<{GK% z5%KZykgGGyH*C+xB&)uoXc1|(0W0fJ!tiDP;D+wtpd0))$~aC?Tjbw$hTQGdnSGCA zgZb;?n7()7ub7dq}?oc~Sa{bZ^)qx(> zMrMW0^+`_x4qDqo1Oda`!St&ef8O5WydB7YNwm~Abb1fB{Qmy_h2WK^BRg*%=ED$- zi7jjcke;sVta!DK>e-_+$*AOtVT2CGSbnpk`nSMCpjdf#`rhsaGU40Zzi+D66pjV< z)qOh2BxAIPBA1;Kl;w9PW6u1Y!aMdkvg~yT%e>2+R*9KS`JNa26}D)4dRkh9J<28! zuB(3RsLLi$x-#9l&OO+B?4G;m$IX^8dZ3-BADL5&xaabtviXA8i{!9&Vf}4slX?1e zSK+j{*{=@5(up$y&yPNq za288oAwFGKcXbqywWaW@gFFH;|;1}^;Vsgoj$1nQsdJEvJo|l)G+l0Wq2U(wK|0F{Za6PrS8RUtCI&(L(MNYj& z6eqTMd4ofCmcM3DOmZ)9o->nozWb_!nG{#JPfA(K>bE_D zuh`4Ed22tdr&ylM z?j4Beiq6ihr{4y10dVuOt6_6qCbq%E$zi%!Me{#XB#Os1gGtR}jIf9F(s})NO|y@^}F1PiAvjKF`GiZmp=g?i;Aczt~kl zF)}RY)3mfrj=Vk*hz1WQ_LxxYS)8-{$}1*f7NmJ5x+j1z!uB;qUen*UP_XBs;|=>} zAdLGx_V5Py2np?@_W^KOm7B-H_wt5UNQRlJRQ*h-xLDwX%0JGwe{$C60||Ng99CTj?ikLjX1 zUWw@p^eHZ8zAfn!ct!1y{TQu;BPYUF=x?uTje4i<%&Ld}wje&9eT}N7RMU~WnWbZ= z7Q1>Kd9H2AKvlTDwb-?0u{il?jUu`|GJ7&&|7&@(lfEG|&#Q^VZ^_1{(5ZQ6M*cn= z16Yqo*@LlsGfzDLXWBQdalB%m+`L)D@=hnn*ApQ}i-6Or!CUSNMbX~UkmVcwC45n{2v^hkwEaWL68nenYL|W}&f@9SGAILu zq)HTftBP%NpiGM{E`SZW!?3FRZ}50I`kT=p&Kic6KdMFL&w$!hL6t~cd_4MP#46HY z40I`X#!WOqk;e?raUjM@yqpUli9t9`NU(;~L%GC;IZK~E9e4w7jYWcQmxf_whYAaB zvtVp#Ba71J=8#K|qAKJ6NQb_HswuTV8eF-XPrFnS@yO!At(qTqFZnMt&;6Aop8tgw znAePx?@de<8%?dIVT3az%Lg~o#1saA^&7IIZ+_d1^)J1AAZ!VY18de4EzUdplzj%XJ@$$g0=4N5nm!Y+uD$PfFhuL$4(E7 z+~Eo2HGHN0M|L_bX5>z;4D)HbNhP>ac%)*N)BYElkHc#jZ5^XoAI6UMG+G12sP?{g z7{lnCJ1j$DMT^6dspUmOt!iwAgV;$%TfrxR9KqvlOJI{}{pP2fE4TT92p~f*PeWE# zb}1@wzS_z3j2K0V^uYL4mPM+KY}1)aM1gbcgptrMe^|0i?wJqM)8~XZ+Q7IB{?;n{ zA}k-z9>W0f$nU~i#I70&I#NZYU?^D=bqrurW)Tk_kQWWz1A0|>k|~Po5FrFfP~UvA z>6c`>Kr<@+0Nt3UPNTs8v`PNM-JMk}>BB0X%+0O}cpO<*dT=F^#o>b`W`@nEg!Z=&8i}37s)_2;F>fc%ARM$vbD_6t^ z+)}73B6Hk72iF0F1V)kVtO}z~nBt}d?Qx+UJGRg~w(C_8tbwHal@~vO&Xyv^oqh(P zs$wMyviF=6{dGLiUM^}(#OT^JTYeBj@d)x_1{egYvO`7R#(qybxK5?sI@R>b?EY)| zhKJwv_dRdp3ezvDTRZ=*1q4qi?j*FU!-fF((q9vG;s+xs4n7LP^WprYEpAl(qsWW0 zz&NWZ!oE+{XNL1E28F?ZoUn#qTK*VtwyVi^<}O_U=+S=jg6xOOJEz^)0M%c9%(5LO zqc0Y3AF8!mt_knH_#7B{Zo{mSviu&*6#u+_G5ok>tKbXPFf@gV!x$RZ1M({Z7`Wmb zUG~A=69M(su0N|#P=y9E+B?SH*Y&Gdp<*MGCM%!>5(ey$VR>lC-E|gEJO878PJJ)h zG6BNWby1d1G6GZw*G^wzxk`P?Ux98hr|Hz4L;H#?pUKE)83L*gP@7zC(Bd%1$-P3)ZvcZN=PheCj(aE}t-DWT*#S2|(n z?Uv%m0rlw*-jdNZ+Wk{6U!Ji*-wSKcNDS=E`<*2<6etd7cSiff)~`;bk)Zx48&2d7 z#Zr=@k7OftSH;!_k>N7sD;^==FNkuT!o(BX5=C&(cYDb$DSy<(T>Vg7&t|KFO=O43 z!f$2}F)}iDmHq^oJ>EZ<7e#03_J&ToPU@l#G%0?vGbWT)%hiO~i~U0qjR^;I@;{J) z?=$ReSWX|PbL_{xv_%*xTOxZL1d5~(f*#hLCYvjfT}DmqX{UaKuH2o*spBBaIF*&r z2t1}yRa7@mjBtMs_(y*wu=*1&it$t`-_4+qH?-Q$`|o|`Wz4y}E@^RJI(0og26kVg zLT5p9ON-{d(Lw0&_+fgz!G!+qKhw)}s!67T$f7qgv;VNr69eJt@wP;eMt@uXUOr9C zG2dVLP+p@m?S~#wwkAgxbvLwIx~p8T7aymC&+f5jBz$CRu}h&>V0D{dws4jpjEA`W zVJP}EcAcxJy;RypVgu$|`8YT@M(ieyr)tO_ODczNIBxI$_wrsQv@^(#0%~{p7yF0% z9;lCFp3hW$qMe+a@Vv(!9nWb^Oo=MOhKu~72k8}o7(RtVan~_!i}o0!2QDcNnS_|g zVs&e#!1}_0T}%9S;i+}c`uW_lvk|2(xIsz$0UQ|-j);gLa5CK|Z$bs@+uEzioOSu!$r^as9M^>4N;yM8?RJ@zNj9s@kx{Jl}M z`5c}GA{>!I8~{BLYuAqE5mqifM~sRMj$~w_O~q8{HkPGPWAEHTLgFKb&tX}h zTIR8}It@H^Mw4wv+fB>RIY|yj+|v0g4Wge4w{!?KnTmZhNE~yFX70tlS5Ud0mh|Sh z(bYqF-@;vFEjsKwkBP_;DxS-@xJci_U}8Bfqrd4y|I{l#+uNF}01O>IojNt`bc9LU ztahE6Nn3n=d};OQ`IoQE7I~6p=uk}{l@J0a8&IX;#sOgvMf$~uqR|3>&5caBG2VN! z2?^?c>}@*@eHJ)(G~nFHOGeJJgqf-}m~be&eiR+=MV=H7+GX(Z@NCM-hc?vIZ2g=c zs`XYSLW1(w`1E9H8HubIP?GSYcK+0Y<8op!xpQ>B2)MlRFpXTlt|ptF0WJtpD4Gip z+1#P3>Dqh*$P^v2cD%2KY$DuiycDcex?hTuh7t8Bb{(azD9@Vnhh%2p!AZk|` zHAL+ARHRB(V4P*`lM$wEK+IFUlj~nWD+N)2yP<)>4!)ZdI1kZA^ue(}sejFw)=Y`< zYT9Ne_O>9Zo>wb5<4;1HkWrRK#(*l-M3C!|?liJeny%0zXlEZl1>cSQbFf$Me?P%9 zS^BRsE4TED1Zh0DP$iv?_fG<;s>`)&&o6&YP8WVM2hH(^OG`^4P?4q4D~jNV`q&1* z)AYPmDJZT`o*{DTPjeWx$6@)AqNEx=;#(|d*9#-!bZjuG++;0#*)#572(b-}MR z!0o#N6eQj>BoySxDPAm$sB$-(idZa@dexsH>~NLdC0u4Kdt*fOnn$4IZu9sELPM->Yio0= zw_ZrZu2wmi7XT5_g?F!C{QK+=jCZ38?ItC$w>dypF)83+*dgSGHPixgaluX!lB4d! z0_Yr`4>Esf{51sPG{<~v*Im*TJ=!HJUx-fxbN;&#!l^MJd1;a2li`v+kd}|z{r=ji z5(@B_kpPXLQPDAI`{E4RE2P+acpQgYI<^%I2U87%(em}=d4Cc)o= zwWo@dT`>oa5uwDkqt^KK#qsg+o@+A7{&1hz@mm~>5WLfAnRsn9vV~|_<12ZzD)U|V z(s1>rH|{!^AIA29-WgI?yeX`I34%=yqP=)BuUEAtDY0$*T;*V}VzOSw7ljg$LosVG zBdsLn9s?on-;nSkZDz>UG&(HXCEYMRza$2Wk!-wH78{uk7Q!3X(o35ipY{PePMsa8 z+euiSZbL7)ginviA~9)(m}n~Q^5ZuT5VK8hJWkt=EM(KdyQJ5r=Nm%HB@O09ni51{ zb^e~wFr_;he^$VAdzVa)_#iJO+h6u5l(1@@kdHXt&cq&XBX$rufQ&MX(q!9pO^~Rw zWm11sZJa)rC$h|m%0q-?QkC2foKH$~La<-POnE@8W-2;O`F?=ni~sX0xD)u17C!dhzo1y{e_nx8;O{H{{a@G(|9J&0?Em{M g{?FG6$29K=B8r_3veMLj&Io*+(=o!9YF`WgAK0P`H~;_u literal 0 HcmV?d00001 diff --git a/res/app_icons/nuke.png b/res/app_icons/nuke.png new file mode 100644 index 0000000000000000000000000000000000000000..423445409618d59ff822dc09e1dac2e4d3e241aa GIT binary patch literal 49012 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

n)d3@oZpYFsmbJyGIDGW; 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 R3jP~ZQC3Z+PRcy&{{SdSemwvH literal 0 HcmV?d00001 diff --git a/res/app_icons/premiere.png b/res/app_icons/premiere.png new file mode 100644 index 0000000000000000000000000000000000000000..eb5b3d1ba2adb45f6feb2c35f19bc1bc49a22b93 GIT binary patch literal 20121 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 \ No newline at end of file diff --git a/res/ftrack/action_icons/ComponentOpen.svg b/res/ftrack/action_icons/ComponentOpen.svg new file mode 100644 index 0000000000..6d4eba6839 --- /dev/null +++ b/res/ftrack/action_icons/ComponentOpen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/CreateFolders.svg b/res/ftrack/action_icons/CreateFolders.svg new file mode 100644 index 0000000000..c07e474e5c --- /dev/null +++ b/res/ftrack/action_icons/CreateFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CreateProjectFolders.svg b/res/ftrack/action_icons/CreateProjectFolders.svg new file mode 100644 index 0000000000..5fa653361e --- /dev/null +++ b/res/ftrack/action_icons/CreateProjectFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg new file mode 100644 index 0000000000..6d73746ed0 --- /dev/null +++ b/res/ftrack/action_icons/CustomAttributes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/DeleteAsset.svg b/res/ftrack/action_icons/DeleteAsset.svg new file mode 100644 index 0000000000..a41ae31d12 --- /dev/null +++ b/res/ftrack/action_icons/DeleteAsset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/JobKiller-512.png b/res/ftrack/action_icons/JobKiller-512.png new file mode 100644 index 0000000000000000000000000000000000000000..059d70b53c4d401354b39acb6c4e2d32622f1ccd GIT binary patch literal 14539 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 BJOcm# literal 0 HcmV?d00001 diff --git a/res/ftrack/action_icons/MultipleNotes-512.png b/res/ftrack/action_icons/MultipleNotes-512.png new file mode 100644 index 0000000000000000000000000000000000000000..cd4a338ce31fd391d3612dac4d403c2e54f95b51 GIT binary patch literal 21216 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 literal 0 HcmV?d00001 diff --git a/res/ftrack/action_icons/SyncToAvalon-512.png b/res/ftrack/action_icons/SyncToAvalon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..8893db8d5e36a5792b3267f24edc9508fb4fb096 GIT binary patch literal 4835 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∨ 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 \ No newline at end of file diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg new file mode 100644 index 0000000000..30b146803e --- /dev/null +++ b/res/ftrack/action_icons/thumbToChildren.svg @@ -0,0 +1,88 @@ + + + + + + diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg new file mode 100644 index 0000000000..254b650306 --- /dev/null +++ b/res/ftrack/action_icons/thumbToParent.svg @@ -0,0 +1,95 @@ + + + + + + From fce4cf0f176870af405c72cb6bcacacc6f00ed06 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 20:53:41 +0200 Subject: [PATCH 084/193] statics server service created --- pype/services/statics_server/__init__.py | 5 ++ .../services/statics_server/statics_server.py | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 pype/services/statics_server/__init__.py create mode 100644 pype/services/statics_server/statics_server.py diff --git a/pype/services/statics_server/__init__.py b/pype/services/statics_server/__init__.py new file mode 100644 index 0000000000..4b2721b18b --- /dev/null +++ b/pype/services/statics_server/__init__.py @@ -0,0 +1,5 @@ +from .statics_server import StaticsServer + + +def tray_init(tray_widget, main_widget): + return StaticsServer() diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py new file mode 100644 index 0000000000..8fb9990355 --- /dev/null +++ b/pype/services/statics_server/statics_server.py @@ -0,0 +1,78 @@ +import os +import socket +import http.server +import socketserver + +from Qt import QtCore +from pypeapp import config, Logger + + +DIRECTORY = os.path.sep.join([os.environ['PYPE_MODULE_ROOT'], 'res']) + + +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=DIRECTORY, **kwargs) + + +class StaticsServer(QtCore.QThread): + """ Measure user's idle time in seconds. + Idle time resets on keyboard/mouse input. + Is able to emit signals at specific time idle. + """ + + def __init__(self): + super(StaticsServer, self).__init__() + self._is_running = False + self._failed = False + self.log = Logger().get_logger(self.__class__.__name__) + try: + self.presets = config.get_presets().get( + 'services', {}).get('statics_server') + except Exception: + self.presets = {'default_port': 8010, 'exclude_ports': []} + + self.port = self.find_port() + + def tray_start(self): + self.start() + + @property + def is_running(self): + return self._is_running + + @property + def failed(self): + return self._failed + + def stop(self): + self._is_running = False + + def run(self): + self._is_running = True + try: + with socketserver.TCPServer(("", self.port), Handler) as httpd: + while self._is_running: + httpd.handle_request() + except Exception: + self._failed = True + self._is_running = False + + def find_port(self): + start_port = self.presets['default_port'] + exclude_ports = self.presets['exclude_ports'] + found_port = None + # port check takes time so it's lowered to 100 ports + for port in range(start_port, start_port+100): + if port in exclude_ports: + continue + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + result = sock.connect_ex(('localhost', port)) + if result != 0: + found_port = port + if found_port is not None: + break + if found_port is None: + return None + os.environ['PYPE_STATICS_SERVER'] = 'http://localhost:{}'.format(found_port) + return found_port From e88b661230a09b93fb5c71e1947291c4dc2d2b4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 20:58:04 +0200 Subject: [PATCH 085/193] changed icon loading in ftrack actions --- pype/ftrack/actions/action_application_loader.py | 3 ++- pype/ftrack/actions/action_clockify_start.py | 4 +++- pype/ftrack/actions/action_clockify_sync.py | 4 +++- pype/ftrack/actions/action_component_open.py | 7 +++---- pype/ftrack/actions/action_create_cust_attrs.py | 6 ++---- pype/ftrack/actions/action_create_folders.py | 6 +++--- pype/ftrack/actions/action_create_project_folders.py | 5 ++--- pype/ftrack/actions/action_delete_asset.py | 7 +++---- pype/ftrack/actions/action_delete_asset_byname.py | 7 +++---- pype/ftrack/actions/action_djvview.py | 4 +++- pype/ftrack/actions/action_job_killer.py | 6 +++--- pype/ftrack/actions/action_multiple_notes.py | 6 +++--- pype/ftrack/actions/action_rv.py | 4 +++- pype/ftrack/actions/action_sync_to_avalon_local.py | 5 ++--- pype/ftrack/actions/action_test.py | 7 +++---- pype/ftrack/actions/action_thumbToChildern.py | 6 +++--- pype/ftrack/actions/action_thumbToParent.py | 6 +++--- 17 files changed, 47 insertions(+), 46 deletions(-) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 50714e4535..61d129464d 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -35,7 +35,8 @@ def registerApp(app, session): label = apptoml.get('ftrack_label', app.get('label', name)) icon = apptoml.get('ftrack_icon', None) description = apptoml.get('description', None) - + if icon: + icon = icon.format(os.environ.get('PYPE_STATICS_SERVER', '')) # register action AppAction( session, label, name, executable, variant, icon, description diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 594ec21b78..9ca104ee90 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -17,7 +17,9 @@ class StartClockify(BaseAction): #: Action description. description = 'Starts timer on clockify' #: roles that are allowed to register this action - icon = 'https://clockify.me/assets/images/clockify-logo.png' + icon = '{}/app_icons/clockify.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: Clockify api clockapi = ClockifyAPI() diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 4cc00225e2..15670be957 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -21,7 +21,9 @@ class SyncClocify(BaseAction): #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] #: icon - icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' + icon = '{}/app_icons/clockify-white.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: CLockifyApi clockapi = ClockifyAPI() diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index c40a04b2fd..d3213c555a 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import subprocess -import os from pype.vendor import ftrack_api from pype.ftrack import BaseAction @@ -15,9 +15,8 @@ class ComponentOpen(BaseAction): # Action label label = 'Open File' # Action icon - icon = ( - 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/' - 'application_go_run-256.png' + icon = '{}/ftrack/action_icons/ComponentOpen.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 9f2564406a..7dd8335ecc 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -114,10 +114,8 @@ class CustomAttributes(BaseAction): description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-4/512/' - 'Bullet_list_menu_lines_points_items_options-512.png' + icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def __init__(self, session): diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 4426fb9650..53e45fd4fd 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -24,10 +24,10 @@ class CreateFolders(BaseAction): label = 'Create Folders' #: Action Icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '698620-icon-105-folder-add-512.png' + icon = '{}/ftrack/action_icons/CreateFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) + db = DbConnector() def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index 66e2e153e6..dfbd3c0f18 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -20,9 +20,8 @@ class CreateProjectFolders(BaseAction): description = 'Creates folder structure' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'buttons-9/512/Button_Add-01.png' + icon = '{}/ftrack/action_icons/CreateProjectFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) pattern_array = re.compile('\[.*\]') diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 838a77570f..eabadecee6 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -1,3 +1,4 @@ +import os import sys import logging from bson.objectid import ObjectId @@ -16,10 +17,8 @@ class DeleteAsset(BaseAction): label = 'Delete Asset/Subsets' #: Action description. description = 'Removes from Avalon with all childs and asset from Ftrack' - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Delete_dustbin_empty_recycle_recycling_remove_trash-512.png' + icon = '{}/ftrack/action_icons/DeleteAsset.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index 9da60ce763..fa966096a8 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -1,3 +1,4 @@ +import os import sys import logging import argparse @@ -17,10 +18,8 @@ class AssetsRemover(BaseAction): description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Clipboard_copy_delete_minus_paste_remove-512.png' + icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: Db db = DbConnector() diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 942aa7a327..d36ad4c797 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -16,7 +16,9 @@ class DJVViewAction(BaseAction): identifier = "djvview-launch-action" label = "DJV View" description = "DJV View Launcher" - icon = "http://a.fsdn.com/allura/p/djv/icon" + icon = '{}/app_icons/djvView.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 25c0c6a489..8334e1a4f5 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -17,9 +18,8 @@ class JobKiller(BaseAction): description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/new-year-resolutions/64/' - 'resolutions-23-512.png' + icon = '{}/ftrack/action_icons/JobKiller-512.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index 2d93f64242..67b215f723 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class MultipleNotes(BaseAction): label = 'Multiple Notes' #: Action description. description = 'Add same note to multiple Asset Versions' - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'mixed-rounded-flat-icon/512/note_1-512.png' + icon = '{}/ftrack/action_icons/MultipleNotes-512.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 15689ae811..a06f4e89a9 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -17,7 +17,9 @@ class RVAction(BaseAction): identifier = "rv.launch.action" label = "rv" description = "rv Launcher" - icon = "https://img.icons8.com/color/48/000000/circled-play.png" + icon = '{}/ftrack/action_icons/RV.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 1056b5ee55..11a9c4cafa 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -50,9 +50,8 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon-512.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub'] diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 36adb99074..5eac6ebcfd 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import collections -import os import json import re @@ -27,9 +27,8 @@ class TestAction(BaseAction): priority = 10000 #: roles that are allowed to register this action role_list = ['Pypeclub'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/hospital-19/512/' - '8_hospital-512.png' + icon = '{}/ftrack/action_icons/TestAction-512.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 5b63ec264f..4e7f1298f5 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class ThumbToChildren(BaseAction): # Action label label = 'Thumbnail to Children' # Action icon - icon = ( - 'https://cdn3.iconfinder.com/data/icons/transfers/100/' - '239322-download_transfer-128.png' + icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index eb5623328e..632d2a50b2 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -14,9 +15,8 @@ class ThumbToParent(BaseAction): # Action label label = 'Thumbnail to Parent' # Action icon - icon = ( - "https://cdn3.iconfinder.com/data/icons/transfers/100/" - "239419-upload_transfer-512.png" + icon = '{}/ftrack/action_icons/thumbToParent.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): From 73b16923120d1514c765196dfd0f535b881c1eb8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:04:55 +0200 Subject: [PATCH 086/193] delete forgotten resource --- res/ftrack/action_icons/new/plus.svg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 res/ftrack/action_icons/new/plus.svg diff --git a/res/ftrack/action_icons/new/plus.svg b/res/ftrack/action_icons/new/plus.svg deleted file mode 100644 index 8c2f0adf00..0000000000 --- a/res/ftrack/action_icons/new/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 6157dff1341a737df73e82c4964c93f706cbfa81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:33:58 +0200 Subject: [PATCH 087/193] update ftrack preactions --- .../actions/action_application_loader.py | 4 +- pype/ftrack/actions/action_start_timer.py | 79 +++++++++++++++++ pype/ftrack/lib/ftrack_action_handler.py | 4 + pype/ftrack/lib/ftrack_app_handler.py | 62 ++----------- pype/ftrack/lib/ftrack_base_handler.py | 88 ++++++++++++++++--- 5 files changed, 171 insertions(+), 66 deletions(-) create mode 100644 pype/ftrack/actions/action_start_timer.py diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 50714e4535..67a158db14 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -35,10 +35,12 @@ def registerApp(app, session): label = apptoml.get('ftrack_label', app.get('label', name)) icon = apptoml.get('ftrack_icon', None) description = apptoml.get('description', None) + preactions = apptoml.get('preactions', []) # register action AppAction( - session, label, name, executable, variant, icon, description + session, label, name, executable, variant, + icon, description, preactions ).register() diff --git a/pype/ftrack/actions/action_start_timer.py b/pype/ftrack/actions/action_start_timer.py new file mode 100644 index 0000000000..d1f4aa3d09 --- /dev/null +++ b/pype/ftrack/actions/action_start_timer.py @@ -0,0 +1,79 @@ +import ftrack_api +from pype.ftrack import BaseAction + + +class StartTimer(BaseAction): + '''Starts timer.''' + + identifier = 'start.timer' + label = 'Start timer' + description = 'Starts timer' + + def discover(self, session, entities, event): + return False + + def _handle_result(*arg): + return + + def launch(self, session, entities, event): + entity = entities[0] + if entity.entity_type.lower() != 'task': + return + self.start_ftrack_timer(entity) + try: + self.start_clockify_timer(entity) + except Exception: + self.log.warning( + 'Failed starting Clockify timer for task: ' + entity['name'] + ) + return + + def start_ftrack_timer(self, task): + user_query = 'User where username is "{}"'.format(self.session.api_user) + user = self.session.query(user_query).one() + self.log.info('Starting Ftrack timer for task: ' + task['name']) + user.start_timer(task, force=True) + self.session.commit() + + def start_clockify_timer(self, task): + # Validate Clockify settings if Clockify is required + clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) + if clockify_timer is None: + return + + from pype.clockify import ClockifyAPI + clockapi = ClockifyAPI() + if clockapi.verify_api() is False: + return + task_type = task['type']['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + desc_items.append(task['name']) + description = '/'.join(desc_items) + + project_id = clockapi.get_project_id(project_name) + tag_ids = [] + tag_ids.append(clockapi.get_tag_id(task_type)) + clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids + ) + self.log.info('Starting Clockify timer for task: ' + task['name']) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + StartTimer(session).register() diff --git a/pype/ftrack/lib/ftrack_action_handler.py b/pype/ftrack/lib/ftrack_action_handler.py index c6d6181c1f..7a25155718 100644 --- a/pype/ftrack/lib/ftrack_action_handler.py +++ b/pype/ftrack/lib/ftrack_action_handler.py @@ -66,6 +66,10 @@ class BaseAction(BaseHandler): self.session, event ) + preactions_launched = self._handle_preactions(self.session, event) + if preactions_launched is False: + return + interface = self._interface( self.session, *args ) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index e4075e9a19..3c2bc418a8 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -23,10 +23,11 @@ class AppAction(BaseHandler): ''' type = 'Application' + preactions = ['start.timer'] def __init__( self, session, label, name, executable, - variant=None, icon=None, description=None + variant=None, icon=None, description=None, preactions=[] ): super().__init__(session) '''Expects a ftrack_api.Session instance''' @@ -44,6 +45,7 @@ class AppAction(BaseHandler): self.variant = variant self.icon = icon self.description = description + self.preactions.extend(preactions) def register(self): '''Registers the action, subscribing the discover and launch topics.''' @@ -117,6 +119,12 @@ class AppAction(BaseHandler): self.session, event ) + preactions_launched = self._handle_preactions( + self.session, event + ) + if preactions_launched is False: + return + response = self.launch( self.session, *args ) @@ -148,25 +156,6 @@ class AppAction(BaseHandler): entity = entities[0] project_name = entity['project']['full_name'] - # Validate Clockify settings if Clockify is required - clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) - if clockify_timer is not None: - from pype.clockify import ClockifyAPI - clockapi = ClockifyAPI() - if clockapi.verify_api() is False: - title = 'Launch message' - header = '# You Can\'t launch **any Application**' - message = ( - '

You don\'t have set Clockify API' - ' key in Clockify settings

' - ) - items = [ - {'type': 'label', 'value': header}, - {'type': 'label', 'value': message} - ] - self.show_interface(event, items, title) - return False - database = pypelib.get_avalon_database() # Get current environments @@ -335,39 +324,6 @@ class AppAction(BaseHandler): } pass - # RUN TIMER IN FTRACK - username = event['source']['user']['username'] - user_query = 'User where username is "{}"'.format(username) - user = session.query(user_query).one() - task = session.query('Task where id is {}'.format(entity['id'])).one() - self.log.info('Starting timer for task: ' + task['name']) - user.start_timer(task, force=True) - - # RUN TIMER IN Clockify - if clockify_timer is not None: - task_type = task['type']['name'] - project_name = task['project']['full_name'] - - def get_parents(entity): - output = [] - if entity.entity_type.lower() == 'project': - return output - output.extend(get_parents(entity['parent'])) - output.append(entity['name']) - - return output - - desc_items = get_parents(task['parent']) - desc_items.append(task['name']) - description = '/'.join(desc_items) - - project_id = clockapi.get_project_id(project_name) - tag_ids = [] - tag_ids.append(clockapi.get_tag_id(task_type)) - clockapi.start_time_entry( - description, project_id, tag_ids=tag_ids - ) - # Change status of task to In progress config = get_config_data() diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 7a04ba329c..aaaf6a12aa 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -25,6 +25,7 @@ class BaseHandler(object): priority = 100 # Type is just for logging purpose (e.g.: Action, Event, Application,...) type = 'No-type' + preactions = [] def __init__(self, session): '''Expects a ftrack_api.Session instance''' @@ -46,18 +47,7 @@ class BaseHandler(object): else: label = '{} {}'.format(self.label, self.variant) try: - if hasattr(self, "role_list") and len(self.role_list) > 0: - username = self.session.api_user - user = self.session.query( - 'User where username is "{}"'.format(username) - ).one() - available = False - for role in user['user_security_roles']: - if role['security_role']['name'] in self.role_list: - available = True - break - if available is False: - raise MissingPermision + self._preregister() start_time = time.perf_counter() func(*args, **kwargs) @@ -119,6 +109,35 @@ class BaseHandler(object): def reset_session(self): self.session.reset() + if hasattr(self, "role_list") and len(self.role_list) > 0: + username = self.session.api_user + user = self.session.query( + 'User where username is "{}"'.format(username) + ).one() + available = False + for role in user['user_security_roles']: + if role['security_role']['name'] in self.role_list: + available = True + break + if available is False: + raise MissingPermision + + # Custom validations + result = self.preregister() + if result is True: + return + msg = "Pre-register conditions were not met" + if isinstance(result, str): + msg = result + raise Exception(msg) + + def preregister(self): + ''' + Preregister conditions. + Registration continues if returns True. + ''' + return True + def register(self): ''' Registers the action, subscribing the discover and launch topics. @@ -227,6 +246,10 @@ class BaseHandler(object): self.session, event ) + preactions_launched = self._handle_preactions(self.session, event) + if preactions_launched is False: + return + interface = self._interface( self.session, *args ) @@ -263,6 +286,47 @@ class BaseHandler(object): ''' raise NotImplementedError() + def _handle_preactions(self, session, event): + # If preactions are not set + if len(self.preactions) == 0: + return True + # If no selection + selection = event.get('data', {}).get('selection', None) + if (selection is None): + return False + # If preactions were already started + if event['data'].get('preactions_launched', None) is True: + return True + + # Launch preactions + for preaction in self.preactions: + event = ftrack_api.event.base.Event( + topic='ftrack.action.launch', + data=dict( + actionIdentifier=preaction, + selection=selection + ), + source=dict( + user=dict(username=session.api_user) + ) + ) + session.event_hub.publish(event, on_error='ignore') + # Relaunch this action + event = ftrack_api.event.base.Event( + topic='ftrack.action.launch', + data=dict( + actionIdentifier=self.identifier, + selection=selection, + preactions_launched=True + ), + source=dict( + user=dict(username=session.api_user) + ) + ) + session.event_hub.publish(event, on_error='ignore') + + return False + def _interface(self, *args): interface = self.interface(*args) if interface: From b364cc443fd8cca03c2f2c4ebb30344ae351e0ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:35:27 +0200 Subject: [PATCH 088/193] removed unnecessary lines and files --- pype/ftrack/actions/action_create_folders.py | 13 ---- .../actions/action_create_project_folders.py | 24 +------ pype/ftrack/actions/action_djvview.py | 18 ----- pype/ftrack/actions/action_rv.py | 16 ----- pype/ftrack/actions/event_collect_entities.py | 72 ------------------- 5 files changed, 1 insertion(+), 142 deletions(-) delete mode 100644 pype/ftrack/actions/event_collect_entities.py diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 4426fb9650..9c8f576e3b 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -3,11 +3,9 @@ import sys import logging import argparse import re -# import json from pype.vendor import ftrack_api from pype.ftrack import BaseAction -# from pype import api as pype, lib as pypelib from avalon import lib as avalonlib from avalon.tools.libraryloader.io_nonsingleton import DbConnector from pypeapp import config, Anatomy @@ -239,17 +237,6 @@ class CreateFolders(BaseAction): output.extend(self.get_notask_children(child)) return output - # def get_presets(self): - # fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] - # filepath = os.path.normpath(os.path.sep.join(fpath_items)) - # presets = dict() - # try: - # with open(filepath) as data_file: - # presets = json.load(data_file) - # except Exception as e: - # self.log.warning('Wasn\'t able to load presets') - # return dict(presets) - def template_format(self, template, data): partial_data = PartialDict(data) diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index 66e2e153e6..dba2a88f02 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -42,7 +42,7 @@ class CreateProjectFolders(BaseAction): else: project = entity['project'] - presets = config.load_presets()['tools']['project_folder_structure'] + presets = config.get_presets()['tools']['project_folder_structure'] try: # Get paths based on presets basic_paths = self.get_path_items(presets) @@ -142,28 +142,6 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - # def load_presets(self): - # preset_items = [ - # pypelib.get_presets_path(), - # 'tools', - # 'project_folder_structure.json' - # ] - # filepath = os.path.sep.join(preset_items) - # - # # Load folder structure template from presets - # presets = dict() - # try: - # with open(filepath) as data_file: - # presets = json.load(data_file) - # except Exception as e: - # msg = 'Unable to load Folder structure preset' - # self.log.warning(msg) - # return { - # 'success': False, - # 'message': msg - # } - # return presets - def get_path_items(self, in_dict): output = [] for key, value in in_dict.items(): diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 942aa7a327..29d4604968 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -23,9 +23,7 @@ class DJVViewAction(BaseAction): '''Expects a ftrack_api.Session instance''' super().__init__(session) self.djv_path = None - self.config_data = None - # self.load_config_data() self.config_data = config.get_presets()['djv_view']['config'] self.set_djv_path() @@ -54,22 +52,6 @@ class DJVViewAction(BaseAction): return True return False - def load_config_data(self): - # path_items = [pypelib.get_presets_path(), 'djv_view', 'config.json'] - path_items = config.get_presets()['djv_view']['config'] - filepath = os.path.sep.join(path_items) - - data = dict() - try: - with open(filepath) as data_file: - data = json.load(data_file) - except Exception as e: - log.warning( - 'Failed to load data from DJV presets file ({})'.format(e) - ) - - self.config_data = data - def set_djv_path(self): for path in self.config_data.get("djv_paths", []): if os.path.exists(path): diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 15689ae811..f07d339e5c 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -39,7 +39,6 @@ class RVAction(BaseAction): ) else: # if not, fallback to config file location - # self.load_config_data() self.config_data = config.get_presets()['djv_view']['config'] self.set_rv_path() @@ -61,21 +60,6 @@ class RVAction(BaseAction): return True return False - def load_config_data(self): - path_items = config.get_presets['rv']['config.json'] - filepath = os.path.sep.join(path_items) - - data = dict() - try: - with open(filepath) as data_file: - data = json.load(data_file) - except Exception as e: - log.warning( - 'Failed to load data from RV presets file ({})'.format(e) - ) - - self.config_data = data - def set_rv_path(self): self.rv_path = self.config_data.get("rv_path") diff --git a/pype/ftrack/actions/event_collect_entities.py b/pype/ftrack/actions/event_collect_entities.py deleted file mode 100644 index 71f2d26ff3..0000000000 --- a/pype/ftrack/actions/event_collect_entities.py +++ /dev/null @@ -1,72 +0,0 @@ -from pype.vendor import ftrack_api -from pype.ftrack import BaseEvent - - -class CollectEntities(BaseEvent): - - priority = 1 - - def _launch(self, event): - entities = self.translate_event(event) - event['data']['entities_object'] = entities - - return - - def translate_event(self, event): - selection = event['data'].get('selection', []) - - entities = list() - for entity in selection: - ent = self.session.get( - self.get_entity_type(entity), - entity.get('entityId') - ) - entities.append(ent) - - return entities - - def get_entity_type(self, entity): - '''Return translated entity type tht can be used with API.''' - # Get entity type and make sure it is lower cased. Most places except - # the component tab in the Sidebar will use lower case notation. - entity_type = entity.get('entityType').replace('_', '').lower() - - for schema in self.session.schemas: - alias_for = schema.get('alias_for') - - if ( - alias_for and isinstance(alias_for, str) and - alias_for.lower() == entity_type - ): - return schema['id'] - - for schema in self.session.schemas: - if schema['id'].lower() == entity_type: - return schema['id'] - - raise ValueError( - 'Unable to translate entity type: {0}.'.format(entity_type) - ) - - def register(self): - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - self.session.event_hub.subscribe( - 'topic=ftrack.action.launch' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - -def register(session, **kw): - '''Register plugin. Called when used as an plugin.''' - if not isinstance(session, ftrack_api.session.Session): - return - - CollectEntities(session).register() From aafd55bd22a0e36eac2e6d2d8c0441ea43a9e956 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:35:36 +0200 Subject: [PATCH 089/193] update job killer --- pype/ftrack/actions/action_job_killer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 25c0c6a489..8cbddecb1c 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -1,6 +1,7 @@ import sys import argparse import logging +import json from pype.vendor import ftrack_api from pype.ftrack import BaseAction @@ -37,14 +38,18 @@ class JobKiller(BaseAction): ).all() items = [] - import json + item_splitter = {'type': 'label', 'value': '---'} for job in jobs: - data = json.loads(job['data']) + try: + data = json.loads(job['data']) + desctiption = data['description'] + except Exception: + desctiption = '*No description*' user = job['user']['username'] created = job['created_at'].strftime('%d.%m.%Y %H:%M:%S') label = '{} - {} - {}'.format( - data['description'], created, user + desctiption, created, user ) item_label = { 'type': 'label', From cc205f35dc85ed3ccdd1c986e09c250bdd63c710 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:36:29 +0200 Subject: [PATCH 090/193] clockify actions moved into clockify module --- pype/clockify/clockify.py | 22 +++++++++++++++++++ .../ftrack_actions}/action_clockify_start.py | 4 +++- .../ftrack_actions}/action_clockify_sync.py | 4 +++- .../launcher_actions}/ClockifyStart.py | 11 +++------- .../launcher_actions}/ClockifySync.py | 14 +++--------- 5 files changed, 34 insertions(+), 21 deletions(-) rename pype/{ftrack/actions => clockify/ftrack_actions}/action_clockify_start.py (96%) rename pype/{ftrack/actions => clockify/ftrack_actions}/action_clockify_sync.py (97%) rename pype/{plugins/launcher/actions => clockify/launcher_actions}/ClockifyStart.py (86%) rename pype/{plugins/launcher/actions => clockify/launcher_actions}/ClockifySync.py (87%) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 17be642be5..0b84bf3953 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -35,6 +35,28 @@ class ClockifyModule: self.set_menu_visibility() + def process_modules(self, modules): + if 'FtrackModule' in modules: + actions_path = os.path.sep.join([ + os.path.dirname(__file__), + 'ftrack_actions' + ]) + current = os.environ('FTRACK_ACTIONS_PATH', '') + if current: + current += os.pathsep + os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path + + if 'AvalonApps' in modules: + from launcher import lib + actions_path = os.path.sep.join([ + os.path.dirname(__file__), + 'launcher_actions' + ]) + current = os.environ.get('AVALON_ACTIONS', '') + if current: + current += os.pathsep + os.environ['AVALON_ACTIONS'] = current + actions_path + def start_timer_check(self): self.bool_thread_check_running = True if self.thread_timer_check is None: diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py similarity index 96% rename from pype/ftrack/actions/action_clockify_start.py rename to pype/clockify/ftrack_actions/action_clockify_start.py index 594ec21b78..9ca104ee90 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/clockify/ftrack_actions/action_clockify_start.py @@ -17,7 +17,9 @@ class StartClockify(BaseAction): #: Action description. description = 'Starts timer on clockify' #: roles that are allowed to register this action - icon = 'https://clockify.me/assets/images/clockify-logo.png' + icon = '{}/app_icons/clockify.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: Clockify api clockapi = ClockifyAPI() diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/clockify/ftrack_actions/action_clockify_sync.py similarity index 97% rename from pype/ftrack/actions/action_clockify_sync.py rename to pype/clockify/ftrack_actions/action_clockify_sync.py index 4cc00225e2..15670be957 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/clockify/ftrack_actions/action_clockify_sync.py @@ -21,7 +21,9 @@ class SyncClocify(BaseAction): #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] #: icon - icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' + icon = '{}/app_icons/clockify-white.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: CLockifyApi clockapi = ClockifyAPI() diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/clockify/launcher_actions/ClockifyStart.py similarity index 86% rename from pype/plugins/launcher/actions/ClockifyStart.py rename to pype/clockify/launcher_actions/ClockifyStart.py index 9183805c7f..6a9ceaec73 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/clockify/launcher_actions/ClockifyStart.py @@ -1,9 +1,7 @@ from avalon import api, io from pype.api import Logger -try: - from pype.clockify import ClockifyAPI -except Exception: - pass +from pype.clockify import ClockifyAPI + log = Logger().get_logger(__name__, "clockify_start") @@ -14,13 +12,10 @@ class ClockifyStart(api.Action): label = "Clockify - Start Timer" icon = "clockify_icon" order = 500 - - exec("try: clockapi = ClockifyAPI()\nexcept: clockapi = None") + clockapi = ClockifyAPI() def is_compatible(self, session): """Return whether the action is compatible with the session""" - if self.clockapi is None: - return False if "AVALON_TASK" in session: return True return False diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/clockify/launcher_actions/ClockifySync.py similarity index 87% rename from pype/plugins/launcher/actions/ClockifySync.py rename to pype/clockify/launcher_actions/ClockifySync.py index 0895da555d..3bf389796f 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/clockify/launcher_actions/ClockifySync.py @@ -1,8 +1,5 @@ from avalon import api, io -try: - from pype.clockify import ClockifyAPI -except Exception: - pass +from pype.clockify import ClockifyAPI from pype.api import Logger log = Logger().get_logger(__name__, "clockify_sync") @@ -13,16 +10,11 @@ class ClockifySync(api.Action): label = "Sync to Clockify" icon = "clockify_white_icon" order = 500 - exec( - "try:\n\tclockapi = ClockifyAPI()" - "\n\thave_permissions = clockapi.validate_workspace_perm()" - "\nexcept:\n\tclockapi = None" - ) + clockapi = ClockifyAPI() + have_permissions = clockapi.validate_workspace_perm() def is_compatible(self, session): """Return whether the action is compatible with the session""" - if self.clockapi is None: - return False return self.have_permissions def process(self, session, **kwargs): From 40fe3b13ab363df4f8f6c9d575b51038640a844b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:37:08 +0200 Subject: [PATCH 091/193] tray_start implemented into modules --- pype/ftrack/ftrack_module.py | 1 + pype/services/idle_manager/__init__.py | 4 +--- pype/services/idle_manager/idle_manager.py | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/ftrack_module.py b/pype/ftrack/ftrack_module.py index 127b39d2fc..fdce0535e8 100644 --- a/pype/ftrack/ftrack_module.py +++ b/pype/ftrack/ftrack_module.py @@ -153,6 +153,7 @@ class FtrackModule: parent_menu.addMenu(self.menu) + def tray_start(self): self.validate() # Definition of visibility of each menu actions diff --git a/pype/services/idle_manager/__init__.py b/pype/services/idle_manager/__init__.py index 7c07d3ebee..f1a87bef41 100644 --- a/pype/services/idle_manager/__init__.py +++ b/pype/services/idle_manager/__init__.py @@ -2,6 +2,4 @@ from .idle_manager import IdleManager def tray_init(tray_widget, main_widget): - manager = IdleManager() - manager.start() - return manager + return IdleManager() diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py index e8ba246121..686da85655 100644 --- a/pype/services/idle_manager/idle_manager.py +++ b/pype/services/idle_manager/idle_manager.py @@ -19,6 +19,9 @@ class IdleManager(QtCore.QThread): self.signal_reset_timer.connect(self._reset_time) self._is_running = False + def tray_start(self): + self.start() + def add_time_signal(self, emit_time, signal): """ If any module want to use IdleManager, need to use add_time_signal :param emit_time: time when signal will be emitted From e5c67bc79e3e674ac16d065fa182b13a98956e4f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 21:37:46 +0200 Subject: [PATCH 092/193] attempt to implement recognizing of failied services --- pype/services/idle_manager/idle_manager.py | 7 +++++++ pype/services/timers_manager/timers_manager.py | 1 + 2 files changed, 8 insertions(+) diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py index 686da85655..f7d7f2b34e 100644 --- a/pype/services/idle_manager/idle_manager.py +++ b/pype/services/idle_manager/idle_manager.py @@ -17,6 +17,7 @@ class IdleManager(QtCore.QThread): super(IdleManager, self).__init__() self.log = Logger().get_logger(self.__class__.__name__) self.signal_reset_timer.connect(self._reset_time) + self._failed = False self._is_running = False def tray_start(self): @@ -33,6 +34,10 @@ class IdleManager(QtCore.QThread): self.time_signals[emit_time] = [] self.time_signals[emit_time].append(signal) + @property + def failed(self): + return self._failed + @property def is_running(self): return self._is_running @@ -63,6 +68,8 @@ class IdleManager(QtCore.QThread): thread_keyboard.signal_stop.emit() thread_keyboard.terminate() thread_keyboard.wait() + self._failed = True + self._is_running = False self.log.info('IdleManager has stopped') diff --git a/pype/services/timers_manager/timers_manager.py b/pype/services/timers_manager/timers_manager.py index 6f10a0ec68..319e4c6910 100644 --- a/pype/services/timers_manager/timers_manager.py +++ b/pype/services/timers_manager/timers_manager.py @@ -25,6 +25,7 @@ class TimersManager(metaclass=Singleton): when user idles for a long time (set in presets). """ modules = [] + failed = False is_running = False last_task = None From 0650262aa25a6c8aba8fa68569a7ec0245a24a8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 22:11:49 +0200 Subject: [PATCH 093/193] icons also changed for sync to avalon in events --- pype/ftrack/actions/action_sync_to_avalon_local.py | 2 +- pype/ftrack/events/action_sync_to_avalon.py | 5 ++--- res/ftrack/action_icons/SyncToAvalon-local.svg | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 res/ftrack/action_icons/SyncToAvalon-local.svg diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 11a9c4cafa..54fd0b47f8 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -50,7 +50,7 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon-512.png'.format( + icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 22358cd775..c22fe8f895 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -49,9 +49,8 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon-512.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def register(self): diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg new file mode 100644 index 0000000000..bf4708e8a5 --- /dev/null +++ b/res/ftrack/action_icons/SyncToAvalon-local.svg @@ -0,0 +1 @@ + \ No newline at end of file From 3d62de20e9d7222a84e136de7bb8cdf10da9c342 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 22:12:13 +0200 Subject: [PATCH 094/193] fixed import in clockify actions --- pype/ftrack/actions/action_clockify_start.py | 1 + pype/ftrack/actions/action_clockify_sync.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 9ca104ee90..e09d0b76e6 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 15670be957..695f7581c0 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging From 0da34304604ab1b4c0e376c1e7a9239258dd2307 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 22:12:43 +0200 Subject: [PATCH 095/193] fixed import in clockify actions --- pype/clockify/ftrack_actions/action_clockify_start.py | 1 + pype/clockify/ftrack_actions/action_clockify_sync.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pype/clockify/ftrack_actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py index 9ca104ee90..e09d0b76e6 100644 --- a/pype/clockify/ftrack_actions/action_clockify_start.py +++ b/pype/clockify/ftrack_actions/action_clockify_start.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging diff --git a/pype/clockify/ftrack_actions/action_clockify_sync.py b/pype/clockify/ftrack_actions/action_clockify_sync.py index 15670be957..695f7581c0 100644 --- a/pype/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/clockify/ftrack_actions/action_clockify_sync.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging From 9fb17f963a87f8b8b76f3bdbfb1ac595f96c7b38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 20 Apr 2019 12:21:57 +0200 Subject: [PATCH 096/193] job killer icon change --- pype/ftrack/actions/action_job_killer.py | 2 +- res/ftrack/action_icons/JobKiller-512.png | Bin 14539 -> 0 bytes res/ftrack/action_icons/JobKiller.svg | 374 ++++++++++++++++++++++ 3 files changed, 375 insertions(+), 1 deletion(-) delete mode 100644 res/ftrack/action_icons/JobKiller-512.png create mode 100644 res/ftrack/action_icons/JobKiller.svg diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 8334e1a4f5..393b6a91a1 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -18,7 +18,7 @@ class JobKiller(BaseAction): description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/JobKiller-512.png'.format( + icon = '{}/ftrack/action_icons/JobKiller.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/res/ftrack/action_icons/JobKiller-512.png b/res/ftrack/action_icons/JobKiller-512.png deleted file mode 100644 index 059d70b53c4d401354b39acb6c4e2d32622f1ccd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14539 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 BJOcm# diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg new file mode 100644 index 0000000000..595c780a9b --- /dev/null +++ b/res/ftrack/action_icons/JobKiller.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a6d2f3357a59c54879d62025144c657b0e8d02b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 20 Apr 2019 12:23:20 +0200 Subject: [PATCH 097/193] ftrack png icons converted to svg --- pype/ftrack/actions/action_multiple_notes.py | 2 +- pype/ftrack/actions/action_test.py | 2 +- pype/ftrack/events/action_sync_to_avalon.py | 2 +- res/ftrack/action_icons/MultipleNotes-512.png | Bin 21216 -> 0 bytes res/ftrack/action_icons/MultipleNotes.svg | 15 ++++ res/ftrack/action_icons/SyncToAvalon-512.png | Bin 4835 -> 0 bytes res/ftrack/action_icons/SyncToAvalon.svg | 67 ++++++++++++++ res/ftrack/action_icons/TestAction-512.png | Bin 14386 -> 0 bytes res/ftrack/action_icons/TestAction.svg | 84 ++++++++++++++++++ 9 files changed, 169 insertions(+), 3 deletions(-) delete mode 100644 res/ftrack/action_icons/MultipleNotes-512.png create mode 100644 res/ftrack/action_icons/MultipleNotes.svg delete mode 100644 res/ftrack/action_icons/SyncToAvalon-512.png create mode 100644 res/ftrack/action_icons/SyncToAvalon.svg delete mode 100644 res/ftrack/action_icons/TestAction-512.png create mode 100644 res/ftrack/action_icons/TestAction.svg diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index 67b215f723..338083fe47 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -16,7 +16,7 @@ class MultipleNotes(BaseAction): label = 'Multiple Notes' #: Action description. description = 'Add same note to multiple Asset Versions' - icon = '{}/ftrack/action_icons/MultipleNotes-512.png'.format( + icon = '{}/ftrack/action_icons/MultipleNotes.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 5eac6ebcfd..dcb9dd32d0 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -27,7 +27,7 @@ class TestAction(BaseAction): priority = 10000 #: roles that are allowed to register this action role_list = ['Pypeclub'] - icon = '{}/ftrack/action_icons/TestAction-512.png'.format( + icon = '{}/ftrack/action_icons/TestAction.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index c22fe8f895..dd9534a764 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -49,7 +49,7 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon-512.png'.format( + icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/res/ftrack/action_icons/MultipleNotes-512.png b/res/ftrack/action_icons/MultipleNotes-512.png deleted file mode 100644 index cd4a338ce31fd391d3612dac4d403c2e54f95b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21216 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 + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/SyncToAvalon-512.png b/res/ftrack/action_icons/SyncToAvalon-512.png deleted file mode 100644 index 8893db8d5e36a5792b3267f24edc9508fb4fb096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4835 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 + + + + + diff --git a/res/ftrack/action_icons/TestAction-512.png b/res/ftrack/action_icons/TestAction-512.png deleted file mode 100644 index 01ad35fd1d135b34a56293cc43b3807e9ecaf200..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14386 zcmcJ0_d`?56Yog~O*&Ypf>cqYNE1*95`jn;5ELmFkSe`OO8`rhDo7Io2qImiD4j&5 zN$AklA$vfWreeXYb*&mXe-I<*;XV1*+%xB-|>uNBZ<2(ld00UAJ@c;mz;7=$( zLj`{A`412QK>iqsP&M)&TgTES^BAXmb-Vb8RXsHHMP}BotfFWbJG50;TpAtujM#ld zhZ76qix8l}bRl3^OE|YX+=@z*8&bPjkB#l3vK0t@AsBn1EbgTsR@cvDaJ`|ja$Kv^ zuYfI7Ft~Pey`aWNw|-N4lOmh7W;IQ5+Qj}?mmdPk@&E0^%vwJ2@y#10b>VA8vuvWy z+Whlr@tU$hLhs5X;&3=4Qn{N*UzhRD@amjOt?0Y&A@4QE{eGhkz4Z1ES5yqiSmN~F zsT*!`x4S?^?d;J?_j?tyGo`*!mMI(?D0EPx54F&>ve)+scXYzGpR&HdNlXc#$X`!= z(oAoNGY>~ZFC`-F7rVb&Eq2SeE_VCJ97+eO)Sfb~_!<1m0Mf8cKo~E)NA%S^X(&Ra zX^!EhIS?jqt8Y^6XLhWgP7Z7ynRag$g&cM`Y(9$R0|2x^sIsxjedy${5Pn+Ja2@p? zC;ko?v9&_;=IKn8FF#lJ3+UFqqr^%902rBgr&rJWCy}e1kMzh`SVf98MQAWy&cFM? z9mD-+@hOJ&_6z{PtY|a&;|=Wra_3t>_F>Zp&mqm&6=BoYc9n-6gXJbJnPYdq(SkFV zM3(S~Ll%R!?OB69zvT#XpX=DGh;kBeygq0LD&yYA=<1x}>w%wlitn+SD$;%(8=d)v zby6$2k)s1XWaYarl>`kOv7(UKB-UFO*>39?3wiZ57Ze+(QUGvzgO^@$U4yLb7Ux~U zY}P93zncWZ_bCBD^1N}ixC;tNbw=G-NrSFRbwiS?rdI`w6ktFPw`^yj+zX*~5nFVL zK}T8ZohB?XGfOrfvYfUBUIKtf4Q#6JqwY3* zk_f;T&{k`8M-9}vq|BC?eJfl(;m76DOv8FVN&p^Ma5Go4sX~-~4RcRnG|&$KDg+!+ zs#K-ds5~1Xj}nrtG8)lPfKz*4o?FB@1!YH2-|KYUZbFpJm{PF=n9CQUG_(lhYI^BnDzR61g0_u04Un;&{%{&>Yul!C=;}16q{MdoatJko*w∨ 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 + + + + + + + + + + + + + + + + + + + From 0453bb5a78d67a1ef568d5b3db6222e6d0d98a77 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Apr 2019 16:51:14 +0200 Subject: [PATCH 098/193] fix(nuke): removing Metadata integration, adding `presets` loading --- pype/templates.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/pype/templates.py b/pype/templates.py index 071426859a..7d12801a00 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -4,7 +4,7 @@ import sys from avalon import io, api as avalon, lib as avalonlib from . import lib # from pypeapp.api import (Templates, Logger, format) -from pypeapp import Logger +from pypeapp import Logger, config, Anatomy log = Logger().get_logger(__name__, os.getenv("AVALON_APP", "pype-config")) @@ -19,7 +19,7 @@ def set_session(): def load_data_from_templates(): """ - Load Templates `contextual` data as singleton object + Load Presets and Anatomy `contextual` data as singleton object [info](https://en.wikipedia.org/wiki/Singleton_pattern) Returns: @@ -31,17 +31,29 @@ def load_data_from_templates(): if not any([ api.Dataflow, api.Anatomy, - api.Colorspace, - api.Metadata + api.Colorspace ] ): - # base = Templates() - t = Templates(type=["anatomy", "metadata", "dataflow", "colorspace"]) - api.Anatomy = t.anatomy - api.Metadata = t.metadata.format() - data = {"metadata": api.Metadata} - api.Dataflow = t.dataflow.format(data) - api.Colorspace = t.colorspace + presets = config.get_presets() + anatomy = Anatomy() + + try: + # try if it is not in projects custom directory + # `{PYPE_PROJECT_CONFIGS}/[PROJECT_NAME]/init.json` + # init.json define preset names to be used + p_init = presets["init"] + colorspace = presets["colorspace"][p_init["colorspace"]] + dataflow = presets["dataflow"][p_init["dataflow"]] + except KeyError: + log.warning("No projects custom preset available...") + colorspace = presets["colorspace"]["default"] + dataflow = presets["dataflow"]["default"] + log.info("Presets `colorspace` and `dataflow` loaded from `default`...") + + api.Anatomy = anatomy + api.Dataflow = dataflow + api.Colorspace = colorspace + log.info("Data from templates were Loaded...") @@ -59,7 +71,6 @@ def reset_data_from_templates(): api.Dataflow = None api.Anatomy = None api.Colorspace = None - api.Metadata = None log.info("Data from templates were Unloaded...") @@ -283,11 +294,12 @@ def get_workdir_template(data=None): load_data_from_templates() anatomy = api.Anatomy + anatomy_filled = anatomy.format(data or get_context_data()) try: - work = anatomy.work.format(data or get_context_data()) + work = anatomy_filled["work"] except Exception as e: log.error("{0} Error in " "get_workdir_template(): {1}".format(__name__, e)) - return os.path.join(work.root, work.folder) + return work["folder"] From b6bcb16f2ada14cb3fd437d2fef1267a39ff74d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Apr 2019 16:52:17 +0200 Subject: [PATCH 099/193] fix(nuke): colorspace to nuke converting to `presets` --- pype/nuke/lib.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index c5cd224e10..cc1ff5bfa7 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -195,8 +195,8 @@ def set_viewers_colorspace(viewer): erased_viewers = [] for v in viewers: - v['viewerProcess'].setValue(str(viewer.viewerProcess)) - if str(viewer.viewerProcess) not in v['viewerProcess'].value(): + v['viewerProcess'].setValue(str(viewer["viewerProcess"])) + if str(viewer["viewerProcess"]) not in v['viewerProcess'].value(): copy_inputs = v.dependencies() copy_knobs = {k: v[k].value() for k in v.knobs() if k not in filter_knobs} @@ -218,7 +218,7 @@ def set_viewers_colorspace(viewer): nv[k].setValue(v) # set viewerProcess - nv['viewerProcess'].setValue(str(viewer.viewerProcess)) + nv['viewerProcess'].setValue(str(viewer["viewerProcess"])) if erased_viewers: log.warning( @@ -229,6 +229,16 @@ def set_viewers_colorspace(viewer): def set_root_colorspace(root_dict): assert isinstance(root_dict, dict), log.error( "set_root_colorspace(): argument should be dictionary") + + # first set OCIO + if nuke.root()["colorManagement"].value() not in str(root_dict["colorManagement"]): + nuke.root()["colorManagement"].setValue(str(root_dict["colorManagement"])) + + # second set ocio version + if nuke.root()["OCIO_config"].value() not in str(root_dict["OCIO_config"]): + nuke.root()["OCIO_config"].setValue(str(root_dict["OCIO_config"])) + + # then set the rest for knob, value in root_dict.items(): if nuke.root()[knob].value() not in value: nuke.root()[knob].setValue(str(value)) @@ -244,20 +254,20 @@ def set_writes_colorspace(write_dict): def set_colorspace(): from pype import api as pype - nuke_colorspace = getattr(pype.Colorspace, "nuke", None) + nuke_colorspace = pype.Colorspace.get("nuke", None) try: - set_root_colorspace(nuke_colorspace.root) + set_root_colorspace(nuke_colorspace["root"]) except AttributeError: log.error( "set_colorspace(): missing `root` settings in template") try: - set_viewers_colorspace(nuke_colorspace.viewer) + set_viewers_colorspace(nuke_colorspace["viewer"]) except AttributeError: log.error( "set_colorspace(): missing `viewer` settings in template") try: - set_writes_colorspace(nuke_colorspace.write) + set_writes_colorspace(nuke_colorspace["write"]) except AttributeError: log.error( "set_colorspace(): missing `write` settings in template") @@ -440,7 +450,7 @@ def get_additional_data(container): def get_write_node_template_attr(node): ''' Gets all defined data from presets - + ''' # get avalon data from node data = dict() From 13e38c3a45f06fbf54568a929b837e9750fccda9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Apr 2019 16:52:53 +0200 Subject: [PATCH 100/193] fix(nuke): adding initialization for presets loading --- pype/nuke/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index cd568ed8a2..69117c3605 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -139,7 +139,7 @@ def install(): menu.install() # load data from templates - # api.load_data_from_templates() + api.load_data_from_templates() def uninstall(): From 0ae5c560e1427508a901641f27f54e553420ca63 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Apr 2019 16:53:38 +0200 Subject: [PATCH 101/193] fix(nuke): adding back `presets` loading on init --- pype/__init__.py | 7 +++---- pype/api.py | 12 +++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pype/__init__.py b/pype/__init__.py index 755ffa9e7a..751faef320 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -10,10 +10,9 @@ import logging log = logging.getLogger(__name__) # # do not delete these are mandatory -# Anatomy = None -# Dataflow = None -# Metadata = None -# Colorspace = None +Anatomy = None +Dataflow = None +Colorspace = None PACKAGE_DIR = os.path.dirname(__file__) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") diff --git a/pype/api.py b/pype/api.py index e42c58fab7..fcdcbce82b 100644 --- a/pype/api.py +++ b/pype/api.py @@ -17,12 +17,11 @@ from .action import ( from pypeapp import Logger -# from . import ( -# Anatomy, -# Colorspace, -# Metadata, -# Dataflow -# ) +from . import ( + Anatomy, + Colorspace, + Dataflow +) from .templates import ( load_data_from_templates, @@ -88,7 +87,6 @@ __all__ = [ # preloaded templates "Anatomy", "Colorspace", - "Metadata", "Dataflow", # QtWidgets From 4a7c62870434ec7086c6d40e39dc49b117f1ffc3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 21 Apr 2019 13:59:50 +0200 Subject: [PATCH 102/193] fix(nuke): improving nuke logging --- pype/nuke/__init__.py | 75 ++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index 69117c3605..376e8f95b8 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -6,6 +6,7 @@ from pyblish import api as pyblish from .. import api from pype.nuke import menu +import logging from .lib import ( create_write_node @@ -44,40 +45,40 @@ if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) -# class NukeHandler(Logger.logging.Handler): -# ''' -# Nuke Handler - emits logs into nuke's script editor. -# warning will emit nuke.warning() -# critical and fatal would popup msg dialog to alert of the error. -# ''' -# -# def __init__(self): -# api.Logger.logging.Handler.__init__(self) -# self.set_name("Pype_Nuke_Handler") -# -# def emit(self, record): -# # Formated message: -# msg = self.format(record) -# -# if record.levelname.lower() in [ -# # "warning", -# "critical", -# "fatal", -# "error" -# ]: -# nuke.message(msg) +class NukeHandler(logging.Handler): + ''' + Nuke Handler - emits logs into nuke's script editor. + warning will emit nuke.warning() + critical and fatal would popup msg dialog to alert of the error. + ''' -# -# '''Adding Nuke Logging Handler''' -# nuke_handler = NukeHandler() -# if nuke_handler.get_name() \ -# not in [handler.get_name() -# for handler in Logger.logging.root.handlers[:]]: -# api.Logger.logging.getLogger().addHandler(nuke_handler) -# api.Logger.logging.getLogger().setLevel(Logger.logging.INFO) -# -# if not self.nLogger: -# self.nLogger = Logger + def __init__(self): + logging.Handler.__init__(self) + self.set_name("Pype_Nuke_Handler") + + def emit(self, record): + # Formated message: + msg = self.format(record) + + if record.levelname.lower() in [ + # "warning", + "critical", + "fatal", + "error" + ]: + nuke.message(msg) + + +'''Adding Nuke Logging Handler''' +nuke_handler = NukeHandler() +if nuke_handler.get_name() \ + not in [handler.get_name() + for handler in logging.root.handlers[:]]: + logging.getLogger().addHandler(nuke_handler) + logging.getLogger().setLevel(logging.INFO) + +if not self.nLogger: + self.nLogger = Logger def reload_config(): @@ -113,11 +114,11 @@ def install(): # api.set_avalon_workdir() # reload_config() - import sys + # import sys - for path in sys.path: - if path.startswith("C:\\Users\\Public"): - sys.path.remove(path) + # for path in sys.path: + # if path.startswith("C:\\Users\\Public"): + # sys.path.remove(path) log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) From 4e9d9c70a29b7dbb3ad69591e72e436f6de55025 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 21 Apr 2019 14:00:22 +0200 Subject: [PATCH 103/193] fix(nuke): loader was not working as Logger obj had changed initialization --- pype/plugins/nuke/load/load_sequence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 577a499954..b4e3cfb8b5 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -8,7 +8,7 @@ import avalon.io as io import nuke from pype.api import Logger -log = Logger.get_looger(__name__, "nuke") +log = Logger().get_logger(__name__, "nuke") @contextlib.contextmanager @@ -226,6 +226,7 @@ class LoadSequence(api.Loader): node, updated_dict ) + log.info("udated to version: {}".format(version.get("name"))) def remove(self, container): From 81a0a1e5608b0140a456b621ce34daa286925d15 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Apr 2019 21:23:26 +0200 Subject: [PATCH 104/193] fix(global): cleaning old stuff from plugin --- pype/plugins/ftrack/publish/integrate_ftrack_instances.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index e166af2954..958a14f4f4 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -26,12 +26,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 'render': 'render', 'nukescript': 'comp', 'review': 'mov'} - exclude = [] - - def process(self, instance): - for ex in self.exclude: - if ex in instance.data['families']: - return self.log.debug('instance {}'.format(instance)) From 77f43343d12cc8a795c46e259a6724a9d758cd6c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Apr 2019 21:25:35 +0200 Subject: [PATCH 105/193] fix(global): converting to anatomy pype2.0 --- .../global/publish/integrate_rendered_frames.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 8e7e2a59c4..b72df226de 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -195,7 +195,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = src_collection.format( "{padding}") % i anatomy_filled = anatomy.format(template_data) - test_dest_files.append(anatomy_filled.render.path) + test_dest_files.append(anatomy_filled["render"]["path"]) dst_collections, remainder = clique.assemble(test_dest_files) dst_collection = dst_collections[0] @@ -223,7 +223,6 @@ class IntegrateFrames(pyblish.api.InstancePlugin): # template_data.pop("frame", None) - anatomy.pop("frame", None) fname = files @@ -239,14 +238,14 @@ class IntegrateFrames(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.render.path + dst = anatomy_filled["render"]["path"] instance.data["transfers"].append([src, dst]) - template_data["frame"] = "#" * anatomy.render.padding + template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) anatomy_filled = anatomy.format(template_data) - path_to_save = anatomy_filled.render.path - template = anatomy.render.fullpath + path_to_save = anatomy_filled["render"]["path"] + template = anatomy.templates["render"]["path"] self.log.debug('ext[1:]: {}'.format(ext[1:])) representation = { From 18fdf224f0abbffb76cf49e247075c53eb681b34 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Apr 2019 21:26:43 +0200 Subject: [PATCH 106/193] fix(nuke): removing rubbish logging stuff --- pype/plugins/nuke/publish/collect_writes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 59434e3bec..ce37774ac9 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,12 +1,9 @@ import os import nuke import pyblish.api -import logging import pype.api as pype -log = logging.get_logger(__name__) - @pyblish.api.log class CollectNukeWrites(pyblish.api.ContextPlugin): From a83049e0d667d18673bd6085240b9c90faf36f86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Apr 2019 16:50:09 +0200 Subject: [PATCH 107/193] feat(global): loader imagesequence changed to simple open file --- pype/plugins/global/load/open_file.py | 58 +++++++++++++++++++ .../plugins/global/load/open_imagesequence.py | 49 ---------------- 2 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 pype/plugins/global/load/open_file.py delete mode 100644 pype/plugins/global/load/open_imagesequence.py diff --git a/pype/plugins/global/load/open_file.py b/pype/plugins/global/load/open_file.py new file mode 100644 index 0000000000..9425eaab04 --- /dev/null +++ b/pype/plugins/global/load/open_file.py @@ -0,0 +1,58 @@ +import sys +import os +import subprocess + +from avalon import api + + +def open(filepath): + """Open file with system default executable""" + if sys.platform.startswith('darwin'): + subprocess.call(('open', filepath)) + elif os.name == 'nt': + os.startfile(filepath) + elif os.name == 'posix': + subprocess.call(('xdg-open', filepath)) + + +class Openfile(api.Loader): + """Open Image Sequence with system default""" + + families = ["write"] + representations = ["*"] + + label = "Open" + order = -10 + icon = "play-circle" + color = "orange" + + def load(self, context, name, namespace, data): + from avalon.vendor import clique + + directory = os.path.dirname(self.fname) + pattern = clique.PATTERNS["frames"] + + files = os.listdir(directory) + representation = context["representation"] + + ext = representation["name"] + path = representation["data"]["path"] + + if ext in ["#"]: + collections, remainder = clique.assemble(files, + patterns=[pattern], + minimum_items=1) + + seqeunce = collections[0] + + first_image = list(seqeunce)[0] + filepath = os.path.normpath(os.path.join(directory, first_image)) + else: + file = [f for f in files + if ext in f + if "#" not in f][0] + filepath = os.path.normpath(os.path.join(directory, file)) + + self.log.info("Opening : {}".format(filepath)) + + open(filepath) diff --git a/pype/plugins/global/load/open_imagesequence.py b/pype/plugins/global/load/open_imagesequence.py deleted file mode 100644 index a910625733..0000000000 --- a/pype/plugins/global/load/open_imagesequence.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -import os -import subprocess - -from avalon import api - - -def open(filepath): - """Open file with system default executable""" - if sys.platform.startswith('darwin'): - subprocess.call(('open', filepath)) - elif os.name == 'nt': - os.startfile(filepath) - elif os.name == 'posix': - subprocess.call(('xdg-open', filepath)) - - -class PlayImageSequence(api.Loader): - """Open Image Sequence with system default""" - - families = ["write"] - representations = ["*"] - - label = "Play sequence" - order = -10 - icon = "play-circle" - color = "orange" - - def load(self, context, name, namespace, data): - - directory = self.fname - from avalon.vendor import clique - - pattern = clique.PATTERNS["frames"] - files = os.listdir(directory) - collections, remainder = clique.assemble(files, - patterns=[pattern], - minimum_items=1) - - assert not remainder, ("There shouldn't have been a remainder for " - "'%s': %s" % (directory, remainder)) - - seqeunce = collections[0] - first_image = list(seqeunce)[0] - filepath = os.path.normpath(os.path.join(directory, first_image)) - - self.log.info("Opening : {}".format(filepath)) - - open(filepath) From 42ad4615aa0abc49650e58407a6fd29c410330c7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Apr 2019 16:55:59 +0200 Subject: [PATCH 108/193] feat(global): integrate frames now add hashes to padding imagesequences only --- .../global/publish/integrate_rendered_frames.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index b72df226de..e814e31640 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -242,11 +242,17 @@ class IntegrateFrames(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) - template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) + if ext[1:] not in ["jpeg", "jpg", "mov", "mp4", "wav"]: + template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) + anatomy_filled = anatomy.format(template_data) path_to_save = anatomy_filled["render"]["path"] template = anatomy.templates["render"]["path"] - self.log.debug('ext[1:]: {}'.format(ext[1:])) + + self.log.debug("path_to_save: {}".format(path_to_save)) + + + representation = { "schema": "pype:representation-2.0", From 7ed5c1c1d16e669330c51dc089c32fd7e6a8a849 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Apr 2019 17:34:16 +0200 Subject: [PATCH 109/193] (fix) forgotten method definition --- pype/ftrack/lib/ftrack_base_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index aaaf6a12aa..24ece4f11d 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -109,6 +109,7 @@ class BaseHandler(object): def reset_session(self): self.session.reset() + def _preregister(self): if hasattr(self, "role_list") and len(self.role_list) > 0: username = self.session.api_user user = self.session.query( From d561d1fa3b17ef9643aedd3e86083c08cb4d3022 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Apr 2019 18:24:49 +0200 Subject: [PATCH 110/193] fix ftrack_api import in start timer --- pype/ftrack/actions/action_start_timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_start_timer.py b/pype/ftrack/actions/action_start_timer.py index d1f4aa3d09..d27908541e 100644 --- a/pype/ftrack/actions/action_start_timer.py +++ b/pype/ftrack/actions/action_start_timer.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseAction From ca92c42cde633913d1a2d35f592bc2f647986a41 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 Apr 2019 11:31:29 +0200 Subject: [PATCH 111/193] initialization of nukestudio integration --- README.md | 25 +- pype/nukestudio/__init__.py | 181 +++++++++ pype/nukestudio/inventory.py | 347 ++++++++++++++++++ pype/nukestudio/lib.py | 242 ++++++++++++ pype/nukestudio/menu.py | 7 + pype/plugins/nukestudio/publish/collect.py | 188 ++++++++++ .../publish/collect_active_project.py | 13 + .../nukestudio/publish/collect_colorspace.py | 27 ++ .../publish/collect_current_file.py | 14 + .../nukestudio/publish/collect_host.py | 13 + .../publish/collect_host_version.py | 12 + .../nukestudio/publish/collect_selection.py | 15 + .../nukestudio/publish/collect_submission.py | 14 + .../nukestudio/publish/extract_review.py | 102 +++++ .../nukestudio/publish/extract_tasks.py | 125 +++++++ .../nukestudio/publish/validate_names.py | 43 +++ .../publish/validate_projectroot.py | 53 +++ .../publish/validate_resolved_paths.py | 29 ++ .../nukestudio/publish/validate_task.py | 58 +++ .../nukestudio/publish/validate_track_item.py | 59 +++ .../nukestudio/publish/validate_viewer_lut.py | 22 ++ .../Python/Startup/pyblish_startup.py | 14 + .../Python/Startup/selection_tracker.py | 9 + 23 files changed, 1598 insertions(+), 14 deletions(-) create mode 100644 pype/nukestudio/__init__.py create mode 100644 pype/nukestudio/inventory.py create mode 100644 pype/nukestudio/lib.py create mode 100644 pype/nukestudio/menu.py create mode 100644 pype/plugins/nukestudio/publish/collect.py create mode 100644 pype/plugins/nukestudio/publish/collect_active_project.py create mode 100644 pype/plugins/nukestudio/publish/collect_colorspace.py create mode 100644 pype/plugins/nukestudio/publish/collect_current_file.py create mode 100644 pype/plugins/nukestudio/publish/collect_host.py create mode 100644 pype/plugins/nukestudio/publish/collect_host_version.py create mode 100644 pype/plugins/nukestudio/publish/collect_selection.py create mode 100644 pype/plugins/nukestudio/publish/collect_submission.py create mode 100644 pype/plugins/nukestudio/publish/extract_review.py create mode 100644 pype/plugins/nukestudio/publish/extract_tasks.py create mode 100644 pype/plugins/nukestudio/publish/validate_names.py create mode 100644 pype/plugins/nukestudio/publish/validate_projectroot.py create mode 100644 pype/plugins/nukestudio/publish/validate_resolved_paths.py create mode 100644 pype/plugins/nukestudio/publish/validate_task.py create mode 100644 pype/plugins/nukestudio/publish/validate_track_item.py create mode 100644 pype/plugins/nukestudio/publish/validate_viewer_lut.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/selection_tracker.py diff --git a/README.md b/README.md index 7cf8c4c0b6..fe0ad70a36 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,20 @@ -The base studio *config* for [Avalon](https://getavalon.github.io/) +he base studio _config_ for [Avalon](https://getavalon.github.io/) Currently this config is dependent on our customised avalon instalation so it won't work with vanilla avalon core. We're working on open sourcing all of the necessary code though. You can still get inspiration or take our individual validators and scripts which should work just fine in other pipelines. - _This configuration acts as a starting point for all pype club clients wth avalon deployment._ - - ### Code convention Below are some of the standard practices applied to this repositories. -- **Etiquette: PEP8** - - All code is written in PEP8. It is recommended you use a linter as you work, flake8 and pylinter are both good options. -- **Etiquette: Napoleon docstrings** - - Any docstrings are made in Google Napoleon format. See [Napoleon](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for details. -- **Etiquette: Semantic Versioning** - - This project follows [semantic versioning](http://semver.org). -- **Etiquette: Underscore means private** - - Anything prefixed with an underscore means that it is internal to wherever it is used. For example, a variable name is only ever used in the parent function or class. A module is not for use by the end-user. In contrast, anything without an underscore is public, but not necessarily part of the API. Members of the API resides in `api.py`. -- **API: Idempotence** - - A public function must be able to be called twice and produce the exact same result. This means no changing of state without restoring previous state when finishing. For example, if a function requires changing the current selection in Autodesk Maya, it must restore the previous selection prior to completing. +- **Etiquette: PEP8** + \- All code is written in PEP8. It is recommended you use a linter as you work, flake8 and pylinter are both good options. +- **Etiquette: Napoleon docstrings** + \- Any docstrings are made in Google Napoleon format. See [Napoleon](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for details. +- **Etiquette: Semantic Versioning** + \- This project follows [semantic versioning](http://semver.org). +- **Etiquette: Underscore means private** + \- Anything prefixed with an underscore means that it is internal to wherever it is used. For example, a variable name is only ever used in the parent function or class. A module is not for use by the end-user. In contrast, anything without an underscore is public, but not necessarily part of the API. Members of the API resides in `api.py`. +- **API: Idempotence** + \- A public function must be able to be called twice and produce the exact same result. This means no changing of state without restoring previous state when finishing. For example, if a function requires changing the current selection in Autodesk Maya, it must restore the previous selection prior to completing. diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py new file mode 100644 index 0000000000..a1ee6dedf7 --- /dev/null +++ b/pype/nukestudio/__init__.py @@ -0,0 +1,181 @@ +import os +import sys +from avalon import api as avalon +from pyblish import api as pyblish + +from .. import api + +from pype.nukestudio import menu + +from .lib import ( + show, + setup, + register_plugins, + add_to_filemenu +) + +import nuke + +from pypeapp import Logger + + +# #removing logger handler created in avalon_core +# for name, handler in [(handler.get_name(), handler) +# for handler in Logger.logging.root.handlers[:]]: +# if "pype" not in str(name).lower(): +# Logger.logging.root.removeHandler(handler) + + +log = Logger().get_logger(__name__, "nuke") + +# log = api.Logger.getLogger(__name__, "nuke") + +AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") + +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "nuke", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "nuke", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "nuke", "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nuke", "inventory") + +self = sys.modules[__name__] +self.nLogger = None + +if os.getenv("PYBLISH_GUI", None): + pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) + + +# class NukeHandler(Logger.logging.Handler): +# ''' +# Nuke Handler - emits logs into nuke's script editor. +# warning will emit nuke.warning() +# critical and fatal would popup msg dialog to alert of the error. +# ''' +# +# def __init__(self): +# api.Logger.logging.Handler.__init__(self) +# self.set_name("Pype_Nuke_Handler") +# +# def emit(self, record): +# # Formated message: +# msg = self.format(record) +# +# if record.levelname.lower() in [ +# # "warning", +# "critical", +# "fatal", +# "error" +# ]: +# nuke.message(msg) + +# +# '''Adding Nuke Logging Handler''' +# nuke_handler = NukeHandler() +# if nuke_handler.get_name() \ +# not in [handler.get_name() +# for handler in Logger.logging.root.handlers[:]]: +# api.Logger.logging.getLogger().addHandler(nuke_handler) +# api.Logger.logging.getLogger().setLevel(Logger.logging.INFO) +# +# if not self.nLogger: +# self.nLogger = Logger + + +def reload_config(): + """Attempt to reload pipeline at run-time. + + CAUTION: This is primarily for development and debugging purposes. + + """ + + import importlib + + for module in ( + "app", + "app.api", + "{}.api".format(AVALON_CONFIG), + "{}.templates".format(AVALON_CONFIG), + "{}.nuke.actions".format(AVALON_CONFIG), + "{}.nuke.templates".format(AVALON_CONFIG), + "{}.nuke.menu".format(AVALON_CONFIG), + "{}.nuke.lib".format(AVALON_CONFIG), + ): + log.info("Reloading module: {}...".format(module)) + try: + module = importlib.import_module(module) + reload(module) + except Exception as e: + log.warning("Cannot reload module: {}".format(e)) + importlib.reload(module) + + +def install(): + + # api.set_avalon_workdir() + # reload_config() + + import sys + + for path in sys.path: + if path.startswith("C:\\Users\\Public"): + sys.path.remove(path) + + log.info("Registering Nuke plug-ins..") + pyblish.register_plugin_path(PUBLISH_PATH) + avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + + pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + + # Disable all families except for the ones we explicitly want to see + family_states = [ + "write", + "review" + ] + + avalon.data["familiesStateDefault"] = False + avalon.data["familiesStateToggled"] = family_states + + menu.install() + + # load data from templates + # api.load_data_from_templates() + + +def uninstall(): + log.info("Deregistering Nuke plug-ins..") + pyblish.deregister_plugin_path(PUBLISH_PATH) + avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) + avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + + pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + + # reset data from templates + api.reset_data_from_templates() + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle node passthrough states on instance toggles.""" + self.log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( + instance, old_value, new_value)) + + from avalon.nuke import ( + viewer_update_and_undo_stop, + add_publish_knob + ) + + # Whether instances should be passthrough based on new value + + with viewer_update_and_undo_stop(): + n = instance[0] + try: + n["publish"].value() + except ValueError: + n = add_publish_knob(n) + log.info(" `Publish` knob was added to write node..") + + n["publish"].setValue(new_value) diff --git a/pype/nukestudio/inventory.py b/pype/nukestudio/inventory.py new file mode 100644 index 0000000000..0d030c64ad --- /dev/null +++ b/pype/nukestudio/inventory.py @@ -0,0 +1,347 @@ +import os + +from pyblish import api + + +# Collection +collect_json_CollectJSON = api.CollectorOrder + 0.1 +collect_source_CollectScene = api.CollectorOrder + 0.1 +collect_scene_version_CollectSceneVersion = api.CollectorOrder + 0.1 +collect_existing_files_CollectExistingFiles = api.CollectorOrder + 0.25 +collect_reviews_CollectReviews = api.CollectorOrder + 0.3 +collect_sorting_CollectSorting = api.CollectorOrder + 0.49 + +# Validation +persist_publish_state_PersistPublishState = api.ValidatorOrder +validate_executables_ValidateFFmpeg = api.ValidatorOrder +validate_processing_ValidateProcessing = api.ValidatorOrder +validate_scene_version_ValidateSceneVersion = api.ValidatorOrder +validate_review_ValidateReview = api.ValidatorOrder + +# Extraction +extract_scene_save_ExtractSceneSave = api.ExtractorOrder - 0.49 +extract_review_ExtractReview = api.ExtractorOrder +extract_review_ExtractReviewTranscode = api.ExtractorOrder + 0.02 +extract_review_ExtractReviewTranscodeNukeStudio = ( + api.ExtractorOrder + 0.02 +) + +# Integration +extract_json_ExtractJSON = api.IntegratorOrder + 1 +copy_to_clipboard_action_Report = api.IntegratorOrder + 1 + +# AfterEffects +aftereffects_collect_render_items_CollectRenderItems = api.CollectorOrder +aftereffects_collect_scene_CollectScene = api.CollectorOrder + +aftereffects_validate_output_path_ValidateOutputPath = api.ValidatorOrder +aftereffects_validate_scene_path_ValidateScenePath = api.ValidatorOrder +aftereffects_validate_unique_comp_renders_ValidateUniqueCompRenders = ( + api.ValidatorOrder +) + +aftereffects_append_deadline_data_AppendDeadlineData = api.ExtractorOrder +aftereffects_append_ftrack_audio_AppendFtrackAudio = api.ExtractorOrder +aftereffects_extract_local_ExtractLocal = api.ExtractorOrder + +# CelAction +celaction_collect_scene_CollectScene = api.CollectorOrder +celaction_collect_render_CollectRender = api.CollectorOrder + 0.1 +celaction_bait_append_ftrack_data_AppendFtrackData = ( + api.CollectorOrder + 0.1 +) +celaction_bait_append_ftrack_asset_name_AppendFtrackAssetName = ( + api.CollectorOrder + 0.1 +) + +celaction_bait_validate_scene_path_ValidateScenePath = ( + api.ValidatorOrder +) + +celaction_bait_append_ftrack_data_AppendFtrackAudio = ( + api.ExtractorOrder +) +celaction_extract_deadline_ExtractDeadline = api.ExtractorOrder +celaction_extract_render_images_ExtractRenderImages = api.ExtractorOrder +celaction_extract_render_images_ExtractRenderMovie = api.ExtractorOrder + 0.1 +celaction_extract_deadline_movie_ExtractDeadlineMovie = ( + api.ExtractorOrder + 0.4 +) + +celaction_bait_integrate_local_render_IntegrateLocal = ( + api.IntegratorOrder +) + +# Deadline +deadline_OnJobFinished_collect_output_CollectOutput = api.CollectorOrder +deadline_OnJobSubmitted_collect_movie_CollectMovie = api.CollectorOrder +deadline_OnJobSubmitted_collect_render_CollectRender = api.CollectorOrder +deadline_collect_family_CollectFamily = api.CollectorOrder + 0.1 +deadline_collect_houdini_parameters_CollectHoudiniParameters = ( + deadline_collect_family_CollectFamily + 0.01 +) +deadline_collect_maya_parameters_CollectMayaParameters = ( + deadline_collect_family_CollectFamily + 0.01 +) +deadline_collect_nuke_parameters_CollectNukeParameters = ( + deadline_collect_family_CollectFamily + 0.01 +) +deadline_collect_houdini_render_CollectHoudiniRender = api.CollectorOrder + 0.4 + +deadline_validate_houdini_parameters_ValidateHoudiniParameters = ( + api.ValidatorOrder +) +deadline_validate_maya_parameters_ValidateMayaParameters = api.ValidatorOrder +deadline_validate_nuke_parameters_ValidateNukeParameters = api.ValidatorOrder + +deadline_extract_ftrack_path_ExtractFtrackPath = api.ExtractorOrder +deadline_extract_houdini_ExtractHoudini = api.ExtractorOrder +deadline_extract_job_name_ExtractJobName = api.ExtractorOrder +deadline_extract_maya_ExtractMaya = api.ExtractorOrder +deadline_extract_nuke_ExtractNuke = api.ExtractorOrder +deadline_extract_suspended_ExtractSuspended = api.ExtractorOrder + +deadline_integrate_collection_IntegrateCollection = api.IntegratorOrder - 0.1 +deadline_bait_integrate_ftrack_thumbnail_IntegrateFtrackThumbnail = ( + api.IntegratorOrder +) +deadline_bait_update_ftrack_status_UpdateFtrackStatus = ( + api.IntegratorOrder + 0.4 +) + + +# Ftrack +ftrack_collect_nukestudio_CollectNukeStudioEntities = api.CollectorOrder + 0.1 +ftrack_collect_nukestudio_CollectNukeStudioProjectData = ( + api.CollectorOrder + 0.1 +) +ftrack_collect_version_CollectVersion = api.CollectorOrder + 0.2 +ftrack_collect_family_CollectFamily = api.CollectorOrder + 0.4 + +ftrack_validate_assets_ValidateAssets = api.ValidatorOrder +ftrack_validate_nuke_settings_ValidateNukeSettings = api.ValidatorOrder +ftrack_validate_nukestudio_ValidateNukeStudioProjectData = api.ValidatorOrder +ftrack_validate_nukestudio_tasks_ValidateNukeStudioTasks = api.ValidatorOrder + +ftrack_extract_components_ExtractCache = api.ExtractorOrder +ftrack_extract_components_ExtractCamera = api.ExtractorOrder +ftrack_extract_components_ExtractGeometry = api.ExtractorOrder +ftrack_extract_components_ExtractGizmo = api.ExtractorOrder +ftrack_extract_components_ExtractImg = api.ExtractorOrder +ftrack_extract_components_ExtractLUT = api.ExtractorOrder +ftrack_extract_components_ExtractMovie = api.ExtractorOrder +ftrack_extract_components_ExtractAudio = api.ExtractorOrder +ftrack_extract_components_ExtractReview = api.ExtractorOrder +ftrack_extract_components_ExtractScene = api.ExtractorOrder +ftrack_extract_entities_ExtractProject = api.ExtractorOrder +ftrack_extract_entities_ExtractEpisode = ( + ftrack_extract_entities_ExtractProject + 0.01 +) +ftrack_extract_entities_ExtractSequence = ( + ftrack_extract_entities_ExtractEpisode + 0.01 +) +ftrack_extract_entities_ExtractShot = ( + ftrack_extract_entities_ExtractSequence + 0.01 +) +ftrack_extract_entities_ExtractLinkAssetbuilds = ( + ftrack_extract_entities_ExtractShot + 0.01 +) +ftrack_extract_entities_ExtractAssetDataNukeStudio = ( + ftrack_extract_entities_ExtractShot + 0.01 +) +ftrack_extract_entities_ExtractTasks = ( + ftrack_extract_entities_ExtractShot + 0.01 +) +ftrack_extract_entities_ExtractCommit = ( + ftrack_extract_entities_ExtractTasks + 0.01 +) +ftrack_extract_entities_ExtractNukeStudio = ( + ftrack_extract_entities_ExtractTasks + 0.01 +) +ftrack_extract_thumbnail_ExtractThumbnailImg = api.ExtractorOrder + 0.1 +ftrack_extract_review_ExtractReview = api.ExtractorOrder + 0.2 +ftrack_extract_components_ExtractComponents = api.ExtractorOrder + 0.4 + +ftrack_integrate_status_IntegrateStatus = api.IntegratorOrder + +ftrack_other_link_source_OtherLinkSource = api.IntegratorOrder + 1 + +# Hiero +hiero_collect_items_CollectItems = api.CollectorOrder + +hiero_validate_names_ValidateNames = api.ValidatorOrder + +hiero_extract_transcode_BumpyboxExtractTranscodeH264 = api.ExtractorOrder - 0.1 +hiero_extract_transcode_BumpyboxExtractTranscodeJPEG = api.ExtractorOrder - 0.1 +hiero_extract_audio_ExtractAudio = api.ExtractorOrder +hiero_extract_ftrack_shot_ExtractFtrackShot = api.ExtractorOrder +hiero_extract_nuke_script_ExtractNukeScript = api.ExtractorOrder +hiero_extract_transcode_ExtractTranscode = api.ExtractorOrder +hiero_extract_ftrack_components_ExtractFtrackComponents = ( + api.ExtractorOrder + 0.1 +) +hiero_extract_ftrack_tasks_ExtractFtrackTasks = api.ExtractorOrder + 0.1 +hiero_extract_ftrack_thumbnail_ExtractFtrackThumbnail = ( + api.ExtractorOrder + 0.1 +) + +# Houdini +houdini_collect_Collect = api.CollectorOrder + +houdini_validate_alembic_ValidateAlembic = api.ValidatorOrder +houdini_validate_dynamics_ValidateDynamics = api.ValidatorOrder +houdini_validate_geometry_ValidateGeometry = api.ValidatorOrder +houdini_validate_mantra_camera_ValidateMantraCamera = api.ValidatorOrder +houdini_validate_mantra_settings_ValidateMantraSettings = api.ValidatorOrder +houdini_validate_output_path_ValidateOutputPath = api.ValidatorOrder + +houdini_extract_scene_save_ExtractSceneSave = api.ExtractorOrder - 0.1 +houdini_extract_local_ExtractLocal = api.ExtractorOrder + +# Maya +maya_collect_framerate_CollectFramerate = api.CollectorOrder - 0.5 +maya_collect_files_CollectFiles = api.CollectorOrder +maya_collect_render_setups_CollectRenderSetups = api.CollectorOrder +maya_collect_sets_CollectSets = api.CollectorOrder +maya_collect_sets_CollectSetsProcess = maya_collect_sets_CollectSets + 0.01 +maya_collect_sets_CollectSetsPublish = maya_collect_sets_CollectSets + 0.01 +maya_collect_playblasts_CollectPlayblasts = api.CollectorOrder +maya_collect_playblasts_CollectPlayblastsProcess = ( + maya_collect_playblasts_CollectPlayblasts + 0.01 +) +maya_collect_playblasts_CollectPlayblastsPublish = ( + maya_collect_playblasts_CollectPlayblasts + 0.01 +) + +maya_modeling_validate_intermediate_shapes_ValidateIntermediateShapes = ( + api.ValidatorOrder +) +maya_modeling_validate_points_ValidatePoints = ( + api.ValidatorOrder +) +maya_modeling_validate_hierarchy_ValidateHierarchy = ( + api.ValidatorOrder +) +maya_modeling_validate_shape_name_ValidateShapeName = ( + api.ValidatorOrder +) +maya_modeling_validate_transforms_ValidateTransforms = ( + api.ValidatorOrder +) +maya_modeling_validate_display_layer_ValidateDisplayLayer = ( + api.ValidatorOrder +) +maya_modeling_validate_smooth_display_ValidateSmoothDisplay = ( + api.ValidatorOrder +) +maya_validate_arnold_setings_ValidateArnoldSettings = api.ValidatorOrder +maya_validate_name_ValidateName = api.ValidatorOrder +maya_validate_render_camera_ValidateRenderCamera = api.ValidatorOrder +maya_validate_render_layer_settings_ValidateRenderLayerSettings = ( + api.ValidatorOrder +) +maya_validate_vray_settings_ValidateVraySettings = api.ValidatorOrder + +maya_validate_scene_modified_ValidateSceneModified = api.ExtractorOrder - 0.49 +maya_extract_alembic_ExtractAlembic = api.ExtractorOrder +maya_extract_formats_ExtractFormats = api.ExtractorOrder +maya_lookdev_extract_construction_history_ExtractConstructionHistory = ( + maya_extract_formats_ExtractFormats - 0.01 +) +maya_modeling_extract_construction_history_ExtractConstructionHistory = ( + maya_extract_formats_ExtractFormats - 0.01 +) +maya_rigging_extract_disconnect_animation_ExtractDisconnectAnimation = ( + maya_extract_formats_ExtractFormats - 0.01 +) +maya_extract_playblast_ExtractPlayblast = api.ExtractorOrder +maya_extract_render_layer_ExtractRenderLayer = api.ExtractorOrder + +# Nuke +nuke_collect_selection_CollectSelection = api.CollectorOrder - 0.1 +nuke_collect_backdrops_CollectBackdrops = api.CollectorOrder + 0.1 +nuke_collect_framerate_CollectFramerate = api.CollectorOrder +nuke_collect_reads_CollectReads = api.CollectorOrder +nuke_collect_write_geo_CollectWriteGeo = api.CollectorOrder +nuke_collect_writes_CollectWrites = api.CollectorOrder +nuke_collect_write_geo_CollectCacheProcess = api.CollectorOrder + 0.01 +nuke_collect_write_geo_CollectCachePublish = api.CollectorOrder + 0.01 +nuke_collect_writes_CollectWritesProcess = api.CollectorOrder + 0.01 +nuke_collect_writes_CollectWritesPublish = api.CollectorOrder + 0.01 +nuke_collect_groups_CollectGroups = api.CollectorOrder + 0.1 + +nuke_validate_datatype_ValidateDatatype = api.ValidatorOrder +nuke_validate_frame_rate_ValidateFrameRate = api.ValidatorOrder +nuke_validate_group_node_ValidateGroupNode = api.ValidatorOrder +nuke_validate_proxy_mode_ValidateProxyMode = api.ValidatorOrder +nuke_validate_read_node_ValidateReadNode = api.ValidatorOrder +nuke_validate_write_node_ValidateWriteNode = api.ValidatorOrder +nuke_validate_write_node_ValidateReviewNodeDuplicate = api.ValidatorOrder +nuke_validate_writegeo_node_ValidateWriteGeoNode = api.ValidatorOrder + +nuke_extract_output_directory_ExtractOutputDirectory = api.ExtractorOrder - 0.1 +nuke_extract_backdrop_ExtractBackdrop = api.ExtractorOrder +nuke_extract_group_ExtractGroup = api.ExtractorOrder +nuke_extract_write_Extract = api.ExtractorOrder +nuke_extract_write_ExtractCache = api.ExtractorOrder +nuke_extract_write_ExtractCamera = api.ExtractorOrder +nuke_extract_write_ExtractGeometry = api.ExtractorOrder +nuke_extract_write_ExtractWrite = api.ExtractorOrder +nuke_extract_review_ExtractReview = api.ExtractorOrder + 0.01 + +# NukeStudio +nukestudio_collect_CollectFramerate = api.CollectorOrder +nukestudio_collect_CollectTrackItems = api.CollectorOrder +nukestudio_collect_CollectTasks = api.CollectorOrder + 0.01 + +nukestudio_validate_names_ValidateNames = api.ValidatorOrder +nukestudio_validate_names_ValidateNamesFtrack = api.ValidatorOrder +nukestudio_validate_projectroot_ValidateProjectRoot = api.ValidatorOrder +nukestudio_validate_resolved_paths_ValidateResolvedPaths = api.ValidatorOrder +nukestudio_validate_task_ValidateImageSequence = api.ValidatorOrder +nukestudio_validate_task_ValidateOutputRange = api.ValidatorOrder +nukestudio_validate_track_item_ValidateTrackItem = api.ValidatorOrder +nukestudio_validate_track_item_ValidateTrackItemFtrack = api.ValidatorOrder +nukestudio_validate_viewer_lut_ValidateViewerLut = api.ValidatorOrder + +nukestudio_extract_review_ExtractReview = api.ExtractorOrder +nukestudio_extract_tasks_ExtractTasks = api.ExtractorOrder + +# RoyalRender +royalrender_collect_CollectMayaSets = api.CollectorOrder + 0.1 +royalrender_collect_CollectNukeWrites = api.CollectorOrder + 0.1 + +royalrender_extract_maya_ExtractMaya = api.ExtractorOrder +royalrender_extract_maya_alembic_ExtractMovie = api.ExtractorOrder +royalrender_extract_nuke_ExtractNuke = api.ExtractorOrder + +# TVPaint +tvpaint_extract_deadline_ExtractDeadline = api.ExtractorOrder - 0.1 +tvpaint_collect_scene_arg_CollectSceneArg = api.CollectorOrder - 0.05 +tvpaint_collect_render_CollectRender = api.CollectorOrder + 0.1 + +tvpaint_validate_scene_path_ValidateScenePath = api.ValidatorOrder + +tvpaint_extract_hobsoft_scene_ExtractHobsoftScene = api.ExtractorOrder + + +def get_order(module, name): + path = get_variable_name(module, name) + + if path not in globals().keys(): + raise KeyError("\"{0}\" could not be found in inventory.".format(path)) + + return globals()[path] + + +def get_variable_name(module, name): + plugins_directory = os.path.abspath( + os.path.join(__file__, "..", "plugins") + ) + + module = os.path.relpath(module, plugins_directory) + path = "{0}{1}".format(module, name) + path = path.replace(".py", "_") + path = path.replace(os.sep, "_") + + return path diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py new file mode 100644 index 0000000000..e2a11dea08 --- /dev/null +++ b/pype/nukestudio/lib.py @@ -0,0 +1,242 @@ +# Standard library +import os +import sys + +# Pyblish libraries +import pyblish.api + +# Host libraries +import hiero + +from PySide2 import (QtWidgets, QtGui) + +# Local libraries +import plugins + +cached_process = None + + +self = sys.modules[__name__] +self._has_been_setup = False +self._has_menu = False +self._registered_gui = None + + +def setup(console=False, port=None, menu=True): + """Setup integration + + Registers Pyblish for Hiero plug-ins and appends an item to the File-menu + + Arguments: + console (bool): Display console with GUI + port (int, optional): Port from which to start looking for an + available port to connect with Pyblish QML, default + provided by Pyblish Integration. + menu (bool, optional): Display file menu in Hiero. + """ + + if self._has_been_setup: + teardown() + + # register bumpybox plugins + pyblish.api.register_plugin_path(r"C:\Users\hubert\CODE\github\pyblish-bumpybox\pyblish_bumpybox\plugins\nukestudio") + + register_plugins() + register_host() + add_submission() + + if menu: + add_to_filemenu() + self._has_menu = True + + self._has_been_setup = True + print("pyblish: Loaded successfully.") + + +def show(): + """Try showing the most desirable GUI + This function cycles through the currently registered + graphical user interfaces, if any, and presents it to + the user. + """ + + return (_discover_gui() or _show_no_gui)() + + +def _discover_gui(): + """Return the most desirable of the currently registered GUIs""" + + # Prefer last registered + guis = reversed(pyblish.api.registered_guis()) + + for gui in list(guis) + ["pyblish_lite"]: + try: + gui = __import__(gui).show + except (ImportError, AttributeError): + continue + else: + return gui + + +def teardown(): + """Remove integration""" + if not self._has_been_setup: + return + + deregister_plugins() + deregister_host() + + if self._has_menu: + remove_from_filemenu() + self._has_menu = False + + self._has_been_setup = False + print("pyblish: Integration torn down successfully") + + +def remove_from_filemenu(): + raise NotImplementedError("Implement me please.") + + +def deregister_plugins(): + # De-register accompanying plugins + plugin_path = os.path.dirname(plugins.__file__) + pyblish.api.deregister_plugin_path(plugin_path) + print("pyblish: Deregistered %s" % plugin_path) + + +def register_host(): + """Register supported hosts""" + pyblish.api.register_host("nukestudio") + + +def deregister_host(): + """De-register supported hosts""" + pyblish.api.deregister_host("nukestudio") + + +def register_plugins(): + # Register accompanying plugins + plugin_path = os.path.dirname(plugins.__file__) + pyblish.api.register_plugin_path(plugin_path) + + +def add_to_filemenu(): + PublishAction() + + +class PyblishSubmission(hiero.exporters.FnSubmission.Submission): + + def __init__(self): + hiero.exporters.FnSubmission.Submission.__init__(self) + + def addToQueue(self): + # Add submission to Hiero module for retrieval in plugins. + hiero.submission = self + show() + + +def add_submission(): + registry = hiero.core.taskRegistry + registry.addSubmission("Pyblish", PyblishSubmission) + + +class PublishAction(QtWidgets.QAction): + def __init__(self): + QtWidgets.QAction.__init__(self, "Publish", None) + self.triggered.connect(self.publish) + + for interest in ["kShowContextMenu/kTimeline", + "kShowContextMenukBin", + "kShowContextMenu/kSpreadsheet"]: + hiero.core.events.registerInterest(interest, self.eventHandler) + + self.setShortcut("Ctrl+Alt+P") + + def publish(self): + import pyblish_nukestudio + + # Removing "submission" attribute from hiero module, to prevent tasks + # from getting picked up when not using the "Export" dialog. + if hasattr(hiero, "submission"): + del hiero.submission + + pyblish_nukestudio.show() + + def eventHandler(self, event): + + # Add the Menu to the right-click menu + event.menu.addAction(self) + + +def _show_no_gui(): + """Popup with information about how to register a new GUI + In the event of no GUI being registered or available, + this information dialog will appear to guide the user + through how to get set up with one. + """ + + messagebox = QtWidgets.QMessageBox() + messagebox.setIcon(messagebox.Warning) + messagebox.setWindowIcon(QtGui.QIcon(os.path.join( + os.path.dirname(pyblish.__file__), + "icons", + "logo-32x32.svg")) + ) + + spacer = QtWidgets.QWidget() + spacer.setMinimumSize(400, 0) + spacer.setSizePolicy(QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Expanding) + + layout = messagebox.layout() + layout.addWidget(spacer, layout.rowCount(), 0, 1, layout.columnCount()) + + messagebox.setWindowTitle("Uh oh") + messagebox.setText("No registered GUI found.") + + if not pyblish.api.registered_guis(): + messagebox.setInformativeText( + "In order to show you a GUI, one must first be registered. " + "Press \"Show details...\" below for information on how to " + "do that.") + + messagebox.setDetailedText( + "Pyblish supports one or more graphical user interfaces " + "to be registered at once, the next acting as a fallback to " + "the previous." + "\n" + "\n" + "For example, to use Pyblish Lite, first install it:" + "\n" + "\n" + "$ pip install pyblish-lite" + "\n" + "\n" + "Then register it, like so:" + "\n" + "\n" + ">>> import pyblish.api\n" + ">>> pyblish.api.register_gui(\"pyblish_lite\")" + "\n" + "\n" + "The next time you try running this, Lite will appear." + "\n" + "See http://api.pyblish.com/register_gui.html for " + "more information.") + + else: + messagebox.setInformativeText( + "None of the registered graphical user interfaces " + "could be found." + "\n" + "\n" + "Press \"Show details\" for more information.") + + messagebox.setDetailedText( + "These interfaces are currently registered." + "\n" + "%s" % "\n".join(pyblish.api.registered_guis())) + + messagebox.setStandardButtons(messagebox.Ok) + messagebox.exec_() diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py new file mode 100644 index 0000000000..9180c924ba --- /dev/null +++ b/pype/nukestudio/menu.py @@ -0,0 +1,7 @@ +import nuke +from avalon.api import Session + +from pype.nuke import lib + + +def install(): diff --git a/pype/plugins/nukestudio/publish/collect.py b/pype/plugins/nukestudio/publish/collect.py new file mode 100644 index 0000000000..c2eeb25235 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect.py @@ -0,0 +1,188 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class CollectFramerate(api.ContextPlugin): + """Collect framerate from selected sequence.""" + + order = inventory.get_order(__file__, "CollectFramerate") + label = "Framerate" + hosts = ["nukestudio"] + + def process(self, context): + for item in context.data.get("selection", []): + context.data["framerate"] = item.sequence().framerate().toFloat() + return + + +class CollectTrackItems(api.ContextPlugin): + """Collect all tasks from submission.""" + + order = inventory.get_order(__file__, "CollectTrackItems") + label = "Track Items" + hosts = ["nukestudio"] + + def process(self, context): + import os + + submission = context.data.get("submission", None) + data = {} + + # Set handles + handles = 0 + if submission: + for task in submission.getLeafTasks(): + + if task._cutHandles: + handles = task._cutHandles + + # Skip audio track items + media_type = "core.Hiero.Python.TrackItem.MediaType.kAudio" + if str(task._item.mediaType()) == media_type: + continue + + item = task._item + if item.name() not in data: + data[item.name()] = {"item": item, "tasks": [task]} + else: + data[item.name()]["tasks"].append(task) + + data[item.name()]["startFrame"] = task.outputRange()[0] + data[item.name()]["endFrame"] = task.outputRange()[1] + else: + for item in context.data.get("selection", []): + # Skip audio track items + # Try/Except is to handle items types, like EffectTrackItem + try: + media_type = "core.Hiero.Python.TrackItem.MediaType.kVideo" + if str(item.mediaType()) != media_type: + continue + except: + continue + + data[item.name()] = { + "item": item, + "tasks": [], + "startFrame": item.timelineIn(), + "endFrame": item.timelineOut() + } + + for key, value in data.iteritems(): + + context.create_instance( + name=key, + item=value["item"], + family="trackItem", + tasks=value["tasks"], + startFrame=value["startFrame"] + handles, + endFrame=value["endFrame"] - handles, + handles=handles + ) + context.create_instance( + name=key + "_review", + item=value["item"], + family="review", + families=["output"], + handles=handles, + output_path=os.path.abspath( + os.path.join( + context.data["activeProject"].path(), + "..", + "workspace", + key + ".mov" + ) + ) + ) + + +class CollectTasks(api.ContextPlugin): + """Collect all tasks from submission.""" + + order = inventory.get_order(__file__, "CollectTasks") + label = "Tasks" + hosts = ["nukestudio"] + + def process(self, context): + import os + import re + + import hiero.exporters as he + import clique + + for parent in context: + if "trackItem" != parent.data["family"]: + continue + + for task in parent.data["tasks"]: + asset_type = None + + hiero_cls = he.FnSymLinkExporter.SymLinkExporter + if isinstance(task, hiero_cls): + asset_type = "img" + movie_formats = [".mov", ".R3D"] + ext = os.path.splitext(task.resolvedExportPath())[1] + if ext in movie_formats: + asset_type = "mov" + + hiero_cls = he.FnTranscodeExporter.TranscodeExporter + if isinstance(task, hiero_cls): + asset_type = "img" + if task.resolvedExportPath().endswith(".mov"): + asset_type = "mov" + + hiero_cls = he.FnNukeShotExporter.NukeShotExporter + if isinstance(task, hiero_cls): + asset_type = "scene" + + hiero_cls = he.FnAudioExportTask.AudioExportTask + if isinstance(task, hiero_cls): + asset_type = "audio" + + # Skip all non supported export types + if not asset_type: + continue + + resolved_path = task.resolvedExportPath() + + # Formatting the basename to not include frame padding or + # extension. + name = os.path.splitext(os.path.basename(resolved_path))[0] + name = name.replace(".", "") + name = name.replace("#", "") + name = re.sub(r"%.*d", "", name) + instance = context.create_instance(name=name, parent=parent) + + instance.data["task"] = task + instance.data["item"] = parent.data["item"] + + instance.data["family"] = "trackItem.task" + instance.data["families"] = [asset_type, "local", "task"] + + label = "{1}/{0} - {2} - local".format( + name, parent, asset_type + ) + instance.data["label"] = label + + instance.data["handles"] = parent.data["handles"] + + # Add collection or output + if asset_type == "img": + collection = None + + if "#" in resolved_path: + head = resolved_path.split("#")[0] + padding = resolved_path.count("#") + tail = resolved_path.split("#")[-1] + + collection = clique.Collection( + head=head, padding=padding, tail=tail + ) + + if "%" in resolved_path: + collection = clique.parse( + resolved_path, pattern="{head}{padding}{tail}" + ) + + instance.data["collection"] = collection + else: + instance.data["output_path"] = resolved_path diff --git a/pype/plugins/nukestudio/publish/collect_active_project.py b/pype/plugins/nukestudio/publish/collect_active_project.py new file mode 100644 index 0000000000..0ac6192e4a --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_active_project.py @@ -0,0 +1,13 @@ +import pyblish.api + + +class CollectActiveProject(pyblish.api.ContextPlugin): + """Inject the active project into context""" + + order = pyblish.api.CollectorOrder - 0.2 + + def process(self, context): + import hiero + + context.data["activeProject"] = hiero.ui.activeSequence().project() + self.log.info("activeProject: {}".format(context.data["activeProject"])) diff --git a/pype/plugins/nukestudio/publish/collect_colorspace.py b/pype/plugins/nukestudio/publish/collect_colorspace.py new file mode 100644 index 0000000000..2b629ba1f7 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_colorspace.py @@ -0,0 +1,27 @@ +import pyblish.api + + +class CollectProjectColorspace(pyblish.api.ContextPlugin): + """get active project color settings""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Project's color settings" + def process(self, context): + import hiero + + project = context.data["activeProject"] + colorspace = {} + colorspace["useOCIOEnvironmentOverride"] = project.useOCIOEnvironmentOverride() + colorspace["lutSetting16Bit"] = project.lutSetting16Bit() + colorspace["lutSetting8Bit"] = project.lutSetting8Bit() + colorspace["lutSettingFloat"] = project.lutSettingFloat() + colorspace["lutSettingLog"] = project.lutSettingLog() + colorspace["lutSettingViewer"] = project.lutSettingViewer() + colorspace["lutSettingWorkingSpace"] = project.lutSettingWorkingSpace() + colorspace["lutUseOCIOForExport"] = project.lutUseOCIOForExport() + colorspace["ocioConfigName"] = project.ocioConfigName() + colorspace["ocioConfigPath"] = project.ocioConfigPath() + + context.data["colorspace"] = colorspace + + self.log.info("context.data[colorspace]: {}".format(context.data["colorspace"])) diff --git a/pype/plugins/nukestudio/publish/collect_current_file.py b/pype/plugins/nukestudio/publish/collect_current_file.py new file mode 100644 index 0000000000..010d4e15ab --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_current_file.py @@ -0,0 +1,14 @@ +import pyblish.api + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder + + def process(self, context): + """Todo, inject the current working file""" + + project = context.data('activeProject') + context.set_data('currentFile', value=project.path()) + self.log.info("currentFile: {}".format(context.data["currentFile"])) diff --git a/pype/plugins/nukestudio/publish/collect_host.py b/pype/plugins/nukestudio/publish/collect_host.py new file mode 100644 index 0000000000..caad4d344a --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_host.py @@ -0,0 +1,13 @@ +import pyblish.api + + +class CollectHost(pyblish.api.ContextPlugin): + """Inject the host into context""" + + order = pyblish.api.CollectorOrder + + def process(self, context): + import pyblish.api + + context.set_data("host", pyblish.api.current_host()) + self.log.info("current host: {}".format(pyblish.api.current_host())) diff --git a/pype/plugins/nukestudio/publish/collect_host_version.py b/pype/plugins/nukestudio/publish/collect_host_version.py new file mode 100644 index 0000000000..267d035f4d --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_host_version.py @@ -0,0 +1,12 @@ +import pyblish.api + + +class CollectHostVersion(pyblish.api.ContextPlugin): + """Inject the hosts version into context""" + + order = pyblish.api.CollectorOrder + + def process(self, context): + import nuke + + context.set_data('hostVersion', value=nuke.NUKE_VERSION_STRING) diff --git a/pype/plugins/nukestudio/publish/collect_selection.py b/pype/plugins/nukestudio/publish/collect_selection.py new file mode 100644 index 0000000000..e22ea79a05 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_selection.py @@ -0,0 +1,15 @@ +import pyblish.api + +import hiero + +class CollectSelection(pyblish.api.ContextPlugin): + """Inject the selection in the context.""" + + order = pyblish.api.CollectorOrder - 0.1 + label = "Selection" + + def process(self, context): + selection = getattr(hiero, "selection") + + self.log.debug("selection: {}".format(selection)) + context.data["selection"] = hiero.selection diff --git a/pype/plugins/nukestudio/publish/collect_submission.py b/pype/plugins/nukestudio/publish/collect_submission.py new file mode 100644 index 0000000000..cd2b855524 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_submission.py @@ -0,0 +1,14 @@ +import pyblish.api + + +class CollectSubmission(pyblish.api.ContextPlugin): + """Collect submisson children.""" + + order = pyblish.api.CollectorOrder - 0.1 + + def process(self, context): + import hiero + + if hasattr(hiero, "submission"): + context.data["submission"] = hiero.submission + self.log.debug("__ submission: {}".format(context.data["submission"])) diff --git a/pype/plugins/nukestudio/publish/extract_review.py b/pype/plugins/nukestudio/publish/extract_review.py new file mode 100644 index 0000000000..2b688cb53c --- /dev/null +++ b/pype/plugins/nukestudio/publish/extract_review.py @@ -0,0 +1,102 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ExtractReview(api.InstancePlugin): + """Extracts movie for review""" + + order = inventory.get_order(__file__, "ExtractReview") + label = "NukeStudio Review" + optional = True + hosts = ["nukestudio"] + families = ["review"] + + def process(self, instance): + import os + import time + + import hiero.core + from hiero.exporters.FnExportUtil import writeSequenceAudioWithHandles + + nukeWriter = hiero.core.nuke.ScriptWriter() + + item = instance.data["item"] + + handles = instance.data["handles"] + + sequence = item.parent().parent() + + output_path = os.path.abspath( + os.path.join( + instance.context.data["currentFile"], "..", "workspace" + ) + ) + + # Generate audio + audio_file = os.path.join( + output_path, "{0}.wav".format(instance.data["name"]) + ) + + writeSequenceAudioWithHandles( + audio_file, + sequence, + item.timelineIn(), + item.timelineOut(), + handles, + handles + ) + + # Generate Nuke script + root_node = hiero.core.nuke.RootNode( + item.timelineIn() - handles, + item.timelineOut() + handles, + fps=sequence.framerate() + ) + + root_node.addProjectSettings(instance.context.data["colorspace"]) + + nukeWriter.addNode(root_node) + + item.addToNukeScript( + script=nukeWriter, + includeRetimes=True, + retimeMethod="Frame", + startHandle=handles, + endHandle=handles + ) + + movie_path = os.path.join( + output_path, "{0}.mov".format(instance.data["name"]) + ) + write_node = hiero.core.nuke.WriteNode(movie_path.replace("\\", "/")) + self.log.info("__ write_node: {0}".format(write_node)) + write_node.setKnob("file_type", "mov") + write_node.setKnob("colorspace", instance.context.data["colorspace"]["lutSettingFloat"]) + write_node.setKnob("meta_codec", "ap4h") + write_node.setKnob("mov64_codec", "ap4h") + write_node.setKnob("mov64_bitrate", 400000) + write_node.setKnob("mov64_bitrate_tolerance", 40000000) + write_node.setKnob("mov64_quality_min", 2) + write_node.setKnob("mov64_quality_max", 31) + write_node.setKnob("mov64_gop_size", 12) + write_node.setKnob("mov64_b_frames", 0) + write_node.setKnob("raw", True ) + write_node.setKnob("mov64_audiofile", audio_file.replace("\\", "/")) + write_node.setKnob("mov32_fps", sequence.framerate()) + nukeWriter.addNode(write_node) + + nukescript_path = movie_path.replace(".mov", ".nk") + nukeWriter.writeToDisk(nukescript_path) + + process = hiero.core.nuke.executeNukeScript( + nukescript_path, + open(movie_path.replace(".mov", ".log"), "w") + ) + + while process.poll() is None: + time.sleep(0.5) + + assert os.path.exists(movie_path), "Creating review failed." + + instance.data["output_path"] = movie_path + instance.data["review_family"] = "mov" diff --git a/pype/plugins/nukestudio/publish/extract_tasks.py b/pype/plugins/nukestudio/publish/extract_tasks.py new file mode 100644 index 0000000000..c841b604f1 --- /dev/null +++ b/pype/plugins/nukestudio/publish/extract_tasks.py @@ -0,0 +1,125 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ExtractTasks(api.InstancePlugin): + """Extract tasks.""" + + order = inventory.get_order(__file__, "ExtractTasks") + label = "Tasks" + hosts = ["nukestudio"] + families = ["trackItem.task"] + optional = True + + def filelink(self, src, dst): + import filecmp + import os + import shutil + + import filelink + + # Compare files to check whether they are the same. + if os.path.exists(dst) and filecmp.cmp(src, dst): + return + + # Remove existing destination file. + if os.path.exists(dst): + os.remove(dst) + + try: + filelink.create(src, dst, filelink.HARDLINK) + self.log.debug("Linking: \"{0}\" to \"{1}\"".format(src, dst)) + except WindowsError as e: + if e.winerror == 17: + self.log.warning( + "File linking failed due to: \"{0}\". " + "Resorting to copying instead.".format(e) + ) + shutil.copy(src, dst) + else: + raise e + + def process(self, instance): + import time + import os + + import hiero.core.nuke as nuke + import hiero.exporters as he + import clique + + task = instance.data["task"] + + hiero_cls = he.FnSymLinkExporter.SymLinkExporter + if isinstance(task, hiero_cls): + src = os.path.join( + task.filepath(), + task.fileName() + ) + # Filelink each image file + if "img" in instance.data["families"]: + collection = clique.parse(src + " []") + for f in os.listdir(os.path.dirname(src)): + f = os.path.join(os.path.dirname(src), f) + + frame_offset = task.outputRange()[0] - task.inputRange()[0] + input_range = ( + int(task.inputRange()[0]), int(task.inputRange()[1]) + 1 + ) + for index in range(*input_range): + dst = task.resolvedExportPath() % (index + frame_offset) + self.filelink(src % index, dst) + # Filelink movie file + if "mov" in instance.data["families"]: + dst = task.resolvedExportPath() + self.filelink(src, dst) + + hiero_cls = he.FnTranscodeExporter.TranscodeExporter + if isinstance(task, hiero_cls): + task.startTask() + while task.taskStep(): + time.sleep(1) + + script_path = task._scriptfile + log_path = script_path.replace(".nk", ".log") + log_file = open(log_path, "w") + process = nuke.executeNukeScript(script_path, log_file, True) + + self.poll(process) + + log_file.close() + + if not task._preset.properties()["keepNukeScript"]: + os.remove(script_path) + os.remove(log_path) + + hiero_cls = he.FnNukeShotExporter.NukeShotExporter + if isinstance(task, hiero_cls): + task.startTask() + while task.taskStep(): + time.sleep(1) + + hiero_cls = he.FnAudioExportTask.AudioExportTask + if isinstance(task, hiero_cls): + task.startTask() + while task.taskStep(): + time.sleep(1) + + # Fill collection with output + if "img" in instance.data["families"]: + collection = instance.data["collection"] + path = os.path.dirname(collection.format()) + for f in os.listdir(path): + file_path = os.path.join(path, f).replace("\\", "/") + if collection.match(file_path): + collection.add(file_path) + + def poll(self, process): + import time + + returnCode = process.poll() + + # if the return code hasn't been set, Nuke is still running + if returnCode is None: + time.sleep(1) + + self.poll(process) diff --git a/pype/plugins/nukestudio/publish/validate_names.py b/pype/plugins/nukestudio/publish/validate_names.py new file mode 100644 index 0000000000..571359a3b7 --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_names.py @@ -0,0 +1,43 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ValidateNames(api.InstancePlugin): + """Validate sequence, video track and track item names. + + When creating output directories with the name of an item, ending with a + whitespace will fail the extraction. + Exact matching to optimize processing. + """ + + order = inventory.get_order(__file__, "ValidateNames") + families = ["trackItem"] + match = api.Exact + label = "Names" + hosts = ["nukestudio"] + + def process(self, instance): + + item = instance.data["item"] + + msg = "Track item \"{0}\" ends with a whitespace." + assert not item.name().endswith(" "), msg.format(item.name()) + + msg = "Video track \"{0}\" ends with a whitespace." + msg = msg.format(item.parent().name()) + assert not item.parent().name().endswith(" "), msg + + msg = "Sequence \"{0}\" ends with a whitespace." + msg = msg.format(item.parent().parent().name()) + assert not item.parent().parent().name().endswith(" "), msg + + +class ValidateNamesFtrack(ValidateNames): + """Validate sequence, video track and track item names. + + Because we are matching the families exactly, we need this plugin to + accommodate for the ftrack family addition. + """ + + order = inventory.get_order(__file__, "ValidateNamesFtrack") + families = ["trackItem", "ftrack"] diff --git a/pype/plugins/nukestudio/publish/validate_projectroot.py b/pype/plugins/nukestudio/publish/validate_projectroot.py new file mode 100644 index 0000000000..459b487bd2 --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_projectroot.py @@ -0,0 +1,53 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class RepairProjectRoot(api.Action): + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + import os + + workspace = os.path.join( + os.path.dirname(context.data["currentFile"]), + "workspace" + ).replace("\\", "/") + + if not os.path.exists(workspace): + os.makedirs(workspace) + + context.data["activeProject"].setProjectRoot(workspace) + + # Need to manually fix the tasks "_projectRoot" attribute, because + # setting the project root is not enough. + submission = context.data.get("submission", None) + if submission: + for task in submission.getLeafTasks(): + task._projectRoot = workspace + + +class ValidateProjectRoot(api.ContextPlugin): + """Validate the project root to the workspace directory.""" + + order = inventory.get_order(__file__, "ValidateProjectRoot") + label = "Project Root" + hosts = ["nukestudio"] + actions = [RepairProjectRoot] + + def process(self, context): + import os + + workspace = os.path.join( + os.path.dirname(context.data["currentFile"]), + "workspace" + ).replace("\\", "/") + project_root = context.data["activeProject"].projectRoot() + + failure_message = ( + 'The project root needs to be "{0}", its currently: "{1}"' + ).format(workspace, project_root) + + assert project_root == workspace, failure_message diff --git a/pype/plugins/nukestudio/publish/validate_resolved_paths.py b/pype/plugins/nukestudio/publish/validate_resolved_paths.py new file mode 100644 index 0000000000..110b8772b5 --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_resolved_paths.py @@ -0,0 +1,29 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ValidateResolvedPaths(api.ContextPlugin): + """Validate there are no overlapping resolved paths.""" + + order = inventory.get_order(__file__, "ValidateResolvedPaths") + label = "Resolved Paths" + hosts = ["nukestudio"] + + def process(self, context): + import os + import collections + + paths = [] + for instance in context: + if "trackItem.task" == instance.data["family"]: + paths.append( + os.path.abspath(instance.data["task"].resolvedExportPath()) + ) + + duplicates = [] + for item, count in collections.Counter(paths).items(): + if count > 1: + duplicates.append(item) + + msg = "Duplicate output paths found: {0}".format(duplicates) + assert not duplicates, msg diff --git a/pype/plugins/nukestudio/publish/validate_task.py b/pype/plugins/nukestudio/publish/validate_task.py new file mode 100644 index 0000000000..a48ae115d8 --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_task.py @@ -0,0 +1,58 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ValidateOutputRange(api.InstancePlugin): + """Validate the output range of the task. + + This compares the output range and clip associated with the task, so see + whether there is a difference. This difference indicates that the user has + selected to export the clip length for the task which is very uncommon to + do. + """ + + order = inventory.get_order(__file__, "ValidateOutputRange") + families = ["trackItem.task"] + label = "Output Range" + hosts = ["nukestudio"] + optional = True + + def process(self, instance): + + task = instance.data["task"] + item = instance.data["parent"] + + output_range = task.outputRange() + first_frame = int(item.data["item"].source().sourceIn()) + last_frame = int(item.data["item"].source().sourceOut()) + clip_duration = last_frame - first_frame + 1 + + difference = clip_duration - output_range[1] + failure_message = ( + 'Looks like you are rendering the clip length for the task ' + 'rather than the cut length. If this is intended, just uncheck ' + 'this validator after resetting, else adjust the export range in ' + 'the "Handles" section of the export dialog.' + ) + assert difference, failure_message + + +class ValidateImageSequence(api.InstancePlugin): + """Validate image sequence output path is setup correctly.""" + + order = inventory.get_order(__file__, "ValidateImageSequence") + families = ["trackItem.task", "img"] + match = api.Subset + label = "Image Sequence" + hosts = ["nukestudio"] + optional = True + + def process(self, instance): + + resolved_path = instance.data["task"].resolvedExportPath() + + msg = ( + "Image sequence output is missing a padding. Please add \"####\" " + "or \"%04d\" to the output templates." + ) + assert "#" in resolved_path or "%" in resolved_path, msg diff --git a/pype/plugins/nukestudio/publish/validate_track_item.py b/pype/plugins/nukestudio/publish/validate_track_item.py new file mode 100644 index 0000000000..3c8b3c6cfd --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_track_item.py @@ -0,0 +1,59 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ValidateTrackItem(api.InstancePlugin): + """Validate the track item to the sequence. + + Exact matching to optimize processing. + """ + + order = inventory.get_order(__file__, "ValidateTrackItem") + families = ["trackItem"] + match = api.Exact + label = "Track Item" + hosts = ["nukestudio"] + optional = True + + def process(self, instance): + + item = instance.data["item"] + self.log.info("__ item: {}".format(item)) + media_source = item.source().mediaSource() + self.log.info("__ media_source: {}".format(media_source)) + + msg = ( + 'A setting does not match between track item "{0}" and sequence ' + '"{1}".'.format(item.name(), item.sequence().name()) + + '\n\nSetting: "{0}".''\n\nTrack item: "{1}".\n\nSequence: "{2}".' + ) + + # Validate format settings. + fmt = item.sequence().format() + assert fmt.width() == media_source.width(), msg.format( + "width", fmt.width(), media_source.width() + ) + assert fmt.height() == media_source.height(), msg.format( + "height", fmt.height(), media_source.height() + ) + assert fmt.pixelAspect() == media_source.pixelAspect(), msg.format( + "pixelAspect", fmt.pixelAspect(), media_source.pixelAspect() + ) + + # Validate framerate setting. + sequence = item.sequence() + source_framerate = media_source.metadata()["foundry.source.framerate"] + assert sequence.framerate() == source_framerate, msg.format( + "framerate", source_framerate, sequence.framerate() + ) + +# +# class ValidateTrackItemFtrack(ValidateTrackItem): +# """Validate the track item to the sequence. +# +# Because we are matching the families exactly, we need this plugin to +# accommodate for the ftrack family addition. +# """ +# +# order = inventory.get_order(__file__, "ValidateTrackItemFtrack") +# families = ["trackItem", "ftrack"] diff --git a/pype/plugins/nukestudio/publish/validate_viewer_lut.py b/pype/plugins/nukestudio/publish/validate_viewer_lut.py new file mode 100644 index 0000000000..c9dc87a95b --- /dev/null +++ b/pype/plugins/nukestudio/publish/validate_viewer_lut.py @@ -0,0 +1,22 @@ +from pyblish import api +from pyblish_bumpybox import inventory + + +class ValidateViewerLut(api.ContextPlugin): + """Validate viewer lut in NukeStudio is the same as in Nuke.""" + + order = inventory.get_order(__file__, "ValidateViewerLut") + label = "Viewer LUT" + hosts = ["nukestudio"] + optional = True + + def process(self, context): + import nuke + import hiero + + # nuke_lut = nuke.ViewerProcess.node()["current"].value() + nukestudio_lut = context.data["activeProject"].lutSettingViewer() + self.log.info("__ nukestudio_lut: {}".format(nukestudio_lut)) + + msg = "Viewer LUT can only be RGB" + assert "RGB" in nukestudio_lut, msg diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py new file mode 100644 index 0000000000..4459be6713 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py @@ -0,0 +1,14 @@ +import traceback + +try: + __import__("pype.nukestudio") + __import__("pyblish") + +except ImportError as e: + print traceback.format_exc() + print("pyblish: Could not load integration: %s " % e) + +else: + # Setup integration + import pype.nukestudio.lib + pype.nukestudio.lib.setup() diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/selection_tracker.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/selection_tracker.py new file mode 100644 index 0000000000..b7e05fed7c --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/selection_tracker.py @@ -0,0 +1,9 @@ +"""Puts the selection project into 'hiero.selection'""" + +import hiero + + +def selectionChanged(event): + hiero.selection = event.sender.selection() + +hiero.core.events.registerInterest('kSelectionChanged', selectionChanged) From 618822356766720fb5a5df25b39632ea21ed14ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 Apr 2019 12:24:55 +0200 Subject: [PATCH 112/193] adding additional integration modules --- pype/nukestudio/__init__.py | 53 +- pype/nukestudio/menu.py | 80 +- .../HieroPlayer/PlayerPresets.hrox | 1108 +++++++++++++++++ .../Python/Startup/SpreadsheetExport.py | 140 +++ .../Python/Startup/setFrameRate.py | 164 +++ .../Python/Startup/version_everywhere.py | 352 ++++++ .../Python/StartupUI/PimpMySpreadsheet.py | 844 +++++++++++++ .../Python/StartupUI/Purge.py | 142 +++ .../StartupUI/nukeStyleKeyboardShortcuts.py | 36 + .../Python/StartupUI/setPosterFrame.py | 45 + .../Startup_old/pyblish_startup.py | 14 + .../Startup_old/selection_tracker.py | 9 + .../pipeline.xml | 198 +++ .../pipeline.xml | 198 +++ .../hiero_plugin_path/Templates/vfx_aces.hrox | 38 + .../Templates/vfx_linear.hrox | 38 + .../Templates/vfx_rec709.hrox | 38 + 17 files changed, 3446 insertions(+), 51 deletions(-) create mode 100644 setup/nukestudio/hiero_plugin_path/HieroPlayer/PlayerPresets.hrox create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/SpreadsheetExport.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/setFrameRate.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/version_everywhere.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/PimpMySpreadsheet.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/Purge.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/setPosterFrame.py create mode 100644 setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py create mode 100644 setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py create mode 100644 setup/nukestudio/hiero_plugin_path/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml create mode 100644 setup/nukestudio/hiero_plugin_path/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py index a1ee6dedf7..f3ef69608f 100644 --- a/pype/nukestudio/__init__.py +++ b/pype/nukestudio/__init__.py @@ -19,27 +19,18 @@ import nuke from pypeapp import Logger -# #removing logger handler created in avalon_core -# for name, handler in [(handler.get_name(), handler) -# for handler in Logger.logging.root.handlers[:]]: -# if "pype" not in str(name).lower(): -# Logger.logging.root.removeHandler(handler) - - log = Logger().get_logger(__name__, "nuke") -# log = api.Logger.getLogger(__name__, "nuke") - AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") PARENT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.dirname(PARENT_DIR) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "nuke", "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "nuke", "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "nuke", "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nuke", "inventory") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "inventory") self = sys.modules[__name__] self.nLogger = None @@ -48,42 +39,6 @@ if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) -# class NukeHandler(Logger.logging.Handler): -# ''' -# Nuke Handler - emits logs into nuke's script editor. -# warning will emit nuke.warning() -# critical and fatal would popup msg dialog to alert of the error. -# ''' -# -# def __init__(self): -# api.Logger.logging.Handler.__init__(self) -# self.set_name("Pype_Nuke_Handler") -# -# def emit(self, record): -# # Formated message: -# msg = self.format(record) -# -# if record.levelname.lower() in [ -# # "warning", -# "critical", -# "fatal", -# "error" -# ]: -# nuke.message(msg) - -# -# '''Adding Nuke Logging Handler''' -# nuke_handler = NukeHandler() -# if nuke_handler.get_name() \ -# not in [handler.get_name() -# for handler in Logger.logging.root.handlers[:]]: -# api.Logger.logging.getLogger().addHandler(nuke_handler) -# api.Logger.logging.getLogger().setLevel(Logger.logging.INFO) -# -# if not self.nLogger: -# self.nLogger = Logger - - def reload_config(): """Attempt to reload pipeline at run-time. diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py index 9180c924ba..b62a20559d 100644 --- a/pype/nukestudio/menu.py +++ b/pype/nukestudio/menu.py @@ -1,7 +1,83 @@ -import nuke from avalon.api import Session -from pype.nuke import lib +from pype.nukestudio import lib +import hiero.core + +try: + from PySide.QtGui import * +except Exception: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + +from hiero.ui import findMenuAction + + +# def install(): + # here is the best place to add menu + from avalon.tools import ( + creator, + publish, + workfiles, + cbloader, + cbsceneinventory, + contextmanager, + libraryloader + ) + + menu_name = os.environ['PYPE_STUDIO_NAME'] + # Grab Hiero's MenuBar + M = hiero.ui.menuBar() + + # Add a Menu to the MenuBar + file_action = None + try: + check_made_menu = findMenuAction(menu_name) + except: + pass + + if not check_made_menu: + menu = M.addMenu(menu_name) + else: + menu = check_made_menu.menu() + + actions = [{ + 'action': QAction(QIcon('icons:Position.png'), 'Set Context', None), + 'function': contextmanager.show + }, + { + 'action': QAction(QIcon('icons:ColorAdd.png'), 'Create...', None), + 'function': creator.show + }, + { + 'action': QAction(QIcon('icons:CopyRectangle.png'), 'Load...', None), + 'function': cbloader.show + }, + { + 'action': QAction(QIcon('icons:Output.png'), 'Publish...', None), + 'function': publish.show + }, + { + 'action': QAction(QIcon('icons:ModifyMetaData.png'), 'Manage...', None), + 'function': cbsceneinventory.show + }, + { + 'action': QAction(QIcon('icons:ColorAdd.png'), 'Library...', None), + 'function': libraryloader.show + }] + + + # Create menu items + for a in actions: + # create action + for k in a.keys(): + if 'action' in k: + action = a[k] + elif 'function' in k: + action.triggered.connect(a[k]) + else: + pass + # add action to menu + menu.addAction(action) diff --git a/setup/nukestudio/hiero_plugin_path/HieroPlayer/PlayerPresets.hrox b/setup/nukestudio/hiero_plugin_path/HieroPlayer/PlayerPresets.hrox new file mode 100644 index 0000000000..ec50e123f0 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/HieroPlayer/PlayerPresets.hrox @@ -0,0 +1,1108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 50 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + + + 2 + 70 + + + 2 + 70 + 17 + + + 126935040 + 70 + -1 + + + 2 + 70 + 2 + + + + + + diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/SpreadsheetExport.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/SpreadsheetExport.py new file mode 100644 index 0000000000..3adea8051c --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/SpreadsheetExport.py @@ -0,0 +1,140 @@ +# This action adds itself to the Spreadsheet View context menu allowing the contents of the Spreadsheet be exported as a CSV file. +# Usage: Right-click in Spreadsheet > "Export as .CSV" +# Note: This only prints the text data that is visible in the active Spreadsheet View. +# If you've filtered text, only the visible text will be printed to the CSV file +# Usage: Copy to ~/.hiero/Python/StartupUI +import hiero.core.events +import hiero.ui +import os, csv +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + + +### Magic Widget Finding Methods - This stuff crawls all the PySide widgets, looking for an answer +def findWidget(w): + global foundryWidgets + if 'Foundry' in w.metaObject().className(): + foundryWidgets += [w] + + for c in w.children(): + findWidget(c) + return foundryWidgets + + +def getFoundryWidgetsWithClassName(filter=None): + global foundryWidgets + foundryWidgets = [] + widgets = [] + app = QApplication.instance() + for w in app.topLevelWidgets(): + findWidget(w) + + filteredWidgets = foundryWidgets + if filter: + filteredWidgets = [] + for widget in foundryWidgets: + if filter in widget.metaObject().className(): + filteredWidgets += [widget] + return filteredWidgets + + +# When right click, get the Sequence Name +def activeSpreadsheetTreeView(): + """ + Does some PySide widget Magic to detect the Active Spreadsheet TreeView. + """ + spreadsheetViews = getFoundryWidgetsWithClassName( + filter='SpreadsheetTreeView') + for spreadSheet in spreadsheetViews: + if spreadSheet.hasFocus(): + activeSpreadSheet = spreadSheet + return activeSpreadSheet + return None + + +#### Adds "Export .CSV" action to the Spreadsheet Context menu #### +class SpreadsheetExportCSVAction(QAction): + def __init__(self): + QAction.__init__(self, "Export as .CSV", None) + self.triggered.connect(self.exportCSVFromActiveSpreadsheetView) + hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet", + self.eventHandler) + self.setIcon(QIcon("icons:FBGridView.png")) + + def eventHandler(self, event): + # Insert the action to the Export CSV menu + event.menu.addAction(self) + + #### The guts!.. Writes a CSV file from a Sequence Object #### + def exportCSVFromActiveSpreadsheetView(self): + + # Get the active QTreeView from the active Spreadsheet + spreadsheetTreeView = activeSpreadsheetTreeView() + + if not spreadsheetTreeView: + return 'Unable to detect the active TreeView.' + seq = hiero.ui.activeView().sequence() + if not seq: + print 'Unable to detect the active Sequence from the activeView.' + return + + # The data model of the QTreeView + model = spreadsheetTreeView.model() + + csvSavePath = os.path.join(QDir.homePath(), 'Desktop', + seq.name() + '.csv') + savePath, filter = QFileDialog.getSaveFileName( + None, + caption="Export Spreadsheet to .CSV as...", + dir=csvSavePath, + filter="*.csv") + print 'Saving To: ' + str(savePath) + + # Saving was cancelled... + if len(savePath) == 0: + return + + # Get the Visible Header Columns from the QTreeView + + #csvHeader = ['Event', 'Status', 'Shot Name', 'Reel', 'Track', 'Speed', 'Src In', 'Src Out','Src Duration', 'Dst In', 'Dst Out', 'Dst Duration', 'Clip', 'Clip Media'] + + # Get a CSV writer object + f = open(savePath, 'w') + csvWriter = csv.writer( + f, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) + + # This is a list of the Column titles + csvHeader = [] + + for col in range(0, model.columnCount()): + if not spreadsheetTreeView.isColumnHidden(col): + csvHeader += [model.headerData(col, Qt.Horizontal)] + + # Write the Header row to the CSV file + csvWriter.writerow(csvHeader) + + # Go through each row/column and print + for row in range(model.rowCount()): + row_data = [] + for col in range(model.columnCount()): + if not spreadsheetTreeView.isColumnHidden(col): + row_data.append( + model.index(row, col, QModelIndex()).data( + Qt.DisplayRole)) + + # Write row to CSV file... + csvWriter.writerow(row_data) + + f.close() + # Conveniently show the CSV file in the native file browser... + QDesktopServices.openUrl( + QUrl('file:///%s' % (os.path.dirname(savePath)))) + + +# Add the action... +csvActions = SpreadsheetExportCSVAction() diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/setFrameRate.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/setFrameRate.py new file mode 100644 index 0000000000..ceb96a6fce --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/setFrameRate.py @@ -0,0 +1,164 @@ +# setFrameRate - adds a Right-click menu to the Project Bin view, allowing multiple BinItems (Clips/Sequences) to have their frame rates set. +# Install in: ~/.hiero/Python/StartupUI +# Requires 1.5v1 or later + +import hiero.core +import hiero.ui +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtCore import * + from PySide2.QtWidgets import * + +# Dialog for setting a Custom frame rate. +class SetFrameRateDialog(QDialog): + + def __init__(self,itemSelection=None,parent=None): + super(SetFrameRateDialog, self).__init__(parent) + self.setWindowTitle("Set Custom Frame Rate") + self.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Fixed ) + layout = QFormLayout() + self._itemSelection = itemSelection + + self._frameRateField = QLineEdit() + self._frameRateField.setToolTip('Enter custom frame rate here.') + self._frameRateField.setValidator(QDoubleValidator(1, 99, 3, self)) + self._frameRateField.textChanged.connect(self._textChanged) + layout.addRow("Enter fps: ",self._frameRateField) + + # Standard buttons for Add/Cancel + self._buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self._buttonbox.accepted.connect(self.accept) + self._buttonbox.rejected.connect(self.reject) + self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) + layout.addRow("",self._buttonbox) + self.setLayout(layout) + + def _updateOkButtonState(self): + # Cancel is always an option but only enable Ok if there is some text. + currentFramerate = float(self.currentFramerateString()) + enableOk = False + enableOk = ((currentFramerate > 0.0) and (currentFramerate <= 250.0)) + print 'enabledOk',enableOk + self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(enableOk) + + def _textChanged(self, newText): + self._updateOkButtonState() + + # Returns the current frame rate as a string + def currentFramerateString(self): + return str(self._frameRateField.text()) + + # Presents the Dialog and sets the Frame rate from a selection + def showDialogAndSetFrameRateFromSelection(self): + + if self._itemSelection is not None: + if self.exec_(): + # For the Undo loop... + + # Construct an TimeBase object for setting the Frame Rate (fps) + fps = hiero.core.TimeBase().fromString(self.currentFramerateString()) + + + # Set the frame rate for the selected BinItmes + for item in self._itemSelection: + item.setFramerate(fps) + return + +# This is just a convenience method for returning QActions with a title, triggered method and icon. +def makeAction(title, method, icon = None): + action = QAction(title,None) + action.setIcon(QIcon(icon)) + + # We do this magic, so that the title string from the action is used to set the frame rate! + def methodWrapper(): + method(title) + + action.triggered.connect( methodWrapper ) + return action + +# Menu which adds a Set Frame Rate Menu to Project Bin view +class SetFrameRateMenu: + + def __init__(self): + self._frameRateMenu = None + self._frameRatesDialog = None + + + # ant: Could use hiero.core.defaultFrameRates() here but messes up with string matching because we seem to mix decimal points + self.frameRates = ['8','12','12.50','15','23.98','24','25','29.97','30','48','50','59.94','60'] + hiero.core.events.registerInterest("kShowContextMenu/kBin", self.binViewEventHandler) + + self.menuActions = [] + + def createFrameRateMenus(self,selection): + selectedClipFPS = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))] + selectedClipFPS = hiero.core.util.uniquify(selectedClipFPS) + sameFrameRate = len(selectedClipFPS)==1 + self.menuActions = [] + for fps in self.frameRates: + if fps in selectedClipFPS: + if sameFrameRate: + self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon="icons:Ticked.png")] + else: + self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon="icons:remove active.png")] + else: + self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=None)] + + # Now add Custom... menu + self.menuActions+=[makeAction('Custom...',self.setFrameRateFromMenuSelection, icon=None)] + + frameRateMenu = QMenu("Set Frame Rate") + for a in self.menuActions: + frameRateMenu.addAction(a) + + return frameRateMenu + + def setFrameRateFromMenuSelection(self, menuSelectionFPS): + + selectedBinItems = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))] + currentProject = selectedBinItems[0].project() + + with currentProject.beginUndo("Set Frame Rate"): + if menuSelectionFPS == 'Custom...': + self._frameRatesDialog = SetFrameRateDialog(itemSelection = selectedBinItems ) + self._frameRatesDialog.showDialogAndSetFrameRateFromSelection() + + else: + for b in selectedBinItems: + b.setFramerate(hiero.core.TimeBase().fromString(menuSelectionFPS)) + + return + + # This handles events from the Project Bin View + def binViewEventHandler(self,event): + if not hasattr(event.sender, 'selection'): + # Something has gone wrong, we should only be here if raised + # by the Bin view which gives a selection. + return + + # Reset the selection to None... + self._selection = None + s = event.sender.selection() + + # Return if there's no Selection. We won't add the Menu. + if s == None: + return + # Filter the selection to BinItems + self._selection = [item for item in s if isinstance(item, hiero.core.BinItem)] + if len(self._selection)==0: + return + # Creating the menu based on items selected, to highlight which frame rates are contained + + self._frameRateMenu = self.createFrameRateMenus(self._selection) + + # Insert the Set Frame Rate Button before the Set Media Colour Transform Action + for action in event.menu.actions(): + if str(action.text()) == "Set Media Colour Transform": + event.menu.insertMenu(action, self._frameRateMenu) + break + +# Instantiate the Menu to get it to register itself. +SetFrameRateMenu = SetFrameRateMenu() \ No newline at end of file diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/version_everywhere.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/version_everywhere.py new file mode 100644 index 0000000000..e85e02bfa5 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/version_everywhere.py @@ -0,0 +1,352 @@ +# version_up_everywhere.py +# Adds action to enable a Clip/Shot to be Min/Max/Next/Prev versioned in all shots used in a Project. +# +# Usage: +# 1) Copy file to /Python/Startup +# 2) Right-click on Clip(s) or Bins containing Clips in in the Bin View, or on Shots in the Timeline/Spreadsheet +# 3) Set Version for all Shots > OPTION to update the version in all shots where the Clip is used in the Project. + +import hiero.core +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + + +def whereAmI(self, searchType='TrackItem'): + """returns a list of TrackItem or Sequnece objects in the Project which contain this Clip. + By default this will return a list of TrackItems where the Clip is used in its project. + You can also return a list of Sequences by specifying the searchType to be 'Sequence'. + Should consider putting this into hiero.core.Clip by default? + + Example usage: + + shotsForClip = clip.whereAmI('TrackItem') + sequencesForClip = clip.whereAmI('Sequence') + """ + proj = self.project() + + if ('TrackItem' not in searchType) and ('Sequence' not in searchType): + print "searchType argument must be 'TrackItem' or 'Sequence'" + return None + + # If user specifies a TrackItem, then it will return + searches = hiero.core.findItemsInProject(proj, searchType) + + if len(searches) == 0: + print 'Unable to find %s in any items of type: %s' % (str(self), + str(searchType)) + return None + + # Case 1: Looking for Shots (trackItems) + clipUsedIn = [] + if isinstance(searches[0], hiero.core.TrackItem): + for shot in searches: + # We have to wrap this in a try/except because it's possible through the Python API for a Shot to exist without a Clip in the Bin + try: + + # For versioning to work, we must look to the BinItem that a Clip is wrapped in. + if shot.source().binItem() == self.binItem(): + clipUsedIn.append(shot) + + # If we throw an exception here its because the Shot did not have a Source Clip in the Bin. + except RuntimeError: + hiero.core.log.info( + 'Unable to find Parent Clip BinItem for Shot: %s, Source:%s' + % (shot, shot.source())) + pass + + # Case 1: Looking for Shots (trackItems) + elif isinstance(searches[0], hiero.core.Sequence): + for seq in searches: + # Iterate tracks > shots... + tracks = seq.items() + for track in tracks: + shots = track.items() + for shot in shots: + if shot.source().binItem() == self.binItem(): + clipUsedIn.append(seq) + + return clipUsedIn + + +# Add whereAmI method to Clip object +hiero.core.Clip.whereAmI = whereAmI + + +#### MAIN VERSION EVERYWHERE GUBBINS ##### +class VersionAllMenu(object): + + # These are a set of action names we can use for operating on multiple Clip/TrackItems + eMaxVersion = "Max Version" + eMinVersion = "Min Version" + eNextVersion = "Next Version" + ePreviousVersion = "Previous Version" + + # This is the title used for the Version Menu title. It's long isn't it? + actionTitle = "Set Version for all Shots" + + def __init__(self): + self._versionEverywhereMenu = None + self._versionActions = [] + + hiero.core.events.registerInterest("kShowContextMenu/kBin", + self.binViewEventHandler) + hiero.core.events.registerInterest("kShowContextMenu/kTimeline", + self.binViewEventHandler) + hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet", + self.binViewEventHandler) + + def showVersionUpdateReportFromShotManifest(self, sequenceShotManifest): + """This just displays an info Message box, based on a Sequence[Shot] manifest dictionary""" + + # Now present an info dialog, explaining where shots were updated + updateReportString = "The following Versions were updated:\n" + for seq in sequenceShotManifest.keys(): + updateReportString += "%s:\n Shots:\n" % (seq.name()) + for shot in sequenceShotManifest[seq]: + updateReportString += ' %s\n (New Version: %s)\n' % ( + shot.name(), shot.currentVersion().name()) + updateReportString += '\n' + + infoBox = QMessageBox(hiero.ui.mainWindow()) + infoBox.setIcon(QMessageBox.Information) + + if len(sequenceShotManifest) <= 0: + infoBox.setText("No Shot Versions were updated") + infoBox.setInformativeText( + "Clip could not be found in any Shots in this Project") + else: + infoBox.setText( + "Versions were updated in %i Sequences of this Project." % + (len(sequenceShotManifest))) + infoBox.setInformativeText("Show Details for more info.") + infoBox.setDetailedText(updateReportString) + + infoBox.exec_() + + def makeVersionActionForSingleClip(self, version): + """This is used to populate the QAction list of Versions when a single Clip is selected in the BinView. + It also triggers the Version Update action based on the version passed to it. + (Not sure if this is good design practice, but it's compact!)""" + action = QAction(version.name(), None) + action.setData(lambda: version) + + def updateAllTrackItems(): + currentClip = version.item() + trackItems = currentClip.whereAmI() + if not trackItems: + return + + proj = currentClip.project() + + # A Sequence-Shot manifest dictionary + sequenceShotManifest = {} + + # Make this all undo-able in a single Group undo + with proj.beginUndo( + "Update All Versions for %s" % currentClip.name()): + for shot in trackItems: + seq = shot.parentSequence() + if seq not in sequenceShotManifest.keys(): + sequenceShotManifest[seq] = [shot] + else: + sequenceShotManifest[seq] += [shot] + shot.setCurrentVersion(version) + + # We also should update the current Version of the selected Clip for completeness... + currentClip.binItem().setActiveVersion(version) + + # Now disaplay a Dialog which informs the user of where and what was changed + self.showVersionUpdateReportFromShotManifest(sequenceShotManifest) + + action.triggered.connect(updateAllTrackItems) + return action + + # This is just a convenience method for returning QActions with a title, triggered method and icon. + def makeAction(self, title, method, icon=None): + action = QAction(title, None) + action.setIcon(QIcon(icon)) + + # We do this magic, so that the title string from the action is used to trigger the version change + def methodWrapper(): + method(title) + + action.triggered.connect(methodWrapper) + return action + + def clipSelectionFromView(self, view): + """Helper method to return a list of Clips in the Active View""" + selection = hiero.ui.activeView().selection() + + if len(selection) == 0: + return None + + if isinstance(view, hiero.ui.BinView): + # We could have a mixture of Bins and Clips selected, so sort of the Clips and Clips inside Bins + clipItems = [ + item.activeItem() for item in selection + if hasattr(item, "activeItem") + and isinstance(item.activeItem(), hiero.core.Clip) + ] + + # We'll also append Bins here, and see if can find Clips inside + bins = [ + item for item in selection if isinstance(item, hiero.core.Bin) + ] + + # We search inside of a Bin for a Clip which is not already in clipBinItems + if len(bins) > 0: + # Grab the Clips inside of a Bin and append them to a list + for bin in bins: + clips = hiero.core.findItemsInBin(bin, 'Clip') + for clip in clips: + if clip not in clipItems: + clipItems.append(clip) + + elif isinstance(view, + (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)): + # Here, we have shots. To get to the Clip froma TrackItem, just call source() + clipItems = [ + item.source() for item in selection if hasattr(item, "source") + and isinstance(item, hiero.core.TrackItem) + ] + + return clipItems + + # This generates the Version Up Everywhere menu + def createVersionEveryWhereMenuForView(self, view): + + versionEverywhereMenu = QMenu(self.actionTitle) + self._versionActions = [] + # We look to the activeView for a selection of Clips + clips = self.clipSelectionFromView(view) + + # And bail if nothing is found + if len(clips) == 0: + return versionEverywhereMenu + + # Now, if we have just one Clip selected, we'll form a special menu, which lists all versions + if len(clips) == 1: + + # Get a reversed list of Versions, so that bigger ones appear at top + versions = list(reversed(clips[0].binItem().items())) + for version in versions: + self._versionActions += [ + self.makeVersionActionForSingleClip(version) + ] + + elif len(clips) > 1: + # We will add Max/Min/Prev/Next options, which can be called on a TrackItem, without the need for a Version object + self._versionActions += [ + self.makeAction( + self.eMaxVersion, + self.setTrackItemVersionForClipSelection, + icon=None) + ] + self._versionActions += [ + self.makeAction( + self.eMinVersion, + self.setTrackItemVersionForClipSelection, + icon=None) + ] + self._versionActions += [ + self.makeAction( + self.eNextVersion, + self.setTrackItemVersionForClipSelection, + icon=None) + ] + self._versionActions += [ + self.makeAction( + self.ePreviousVersion, + self.setTrackItemVersionForClipSelection, + icon=None) + ] + + for act in self._versionActions: + versionEverywhereMenu.addAction(act) + + return versionEverywhereMenu + + def setTrackItemVersionForClipSelection(self, versionOption): + + view = hiero.ui.activeView() + if not view: + return + + clipSelection = self.clipSelectionFromView(view) + + if len(clipSelection) == 0: + return + + proj = clipSelection[0].project() + + # Create a Sequence-Shot Manifest, to report to users where a Shot was updated + sequenceShotManifest = {} + + with proj.beginUndo("Update multiple Versions"): + for clip in clipSelection: + + # Look to see if it exists in a TrackItem somewhere... + shotUsage = clip.whereAmI('TrackItem') + + # Next, depending on the versionOption, make the appropriate update + # There's probably a more neat/compact way of doing this... + for shot in shotUsage: + + # This step is done for reporting reasons + seq = shot.parentSequence() + if seq not in sequenceShotManifest.keys(): + sequenceShotManifest[seq] = [shot] + else: + sequenceShotManifest[seq] += [shot] + + if versionOption == self.eMaxVersion: + shot.maxVersion() + elif versionOption == self.eMinVersion: + shot.minVersion() + elif versionOption == self.eNextVersion: + shot.nextVersion() + elif versionOption == self.ePreviousVersion: + shot.prevVersion() + + # Finally, for completeness, set the Max/Min version of the Clip too (if chosen) + # Note: It doesn't make sense to do Next/Prev on a Clip here because next/prev means different things for different Shots + if versionOption == self.eMaxVersion: + clip.binItem().maxVersion() + elif versionOption == self.eMinVersion: + clip.binItem().minVersion() + + # Now disaplay a Dialog which informs the user of where and what was changed + self.showVersionUpdateReportFromShotManifest(sequenceShotManifest) + + # This handles events from the Project Bin View + def binViewEventHandler(self, event): + + if not hasattr(event.sender, 'selection'): + # Something has gone wrong, we should only be here if raised + # by the Bin view which gives a selection. + return + selection = event.sender.selection() + + # Return if there's no Selection. We won't add the Localise Menu. + if selection == None: + return + + view = hiero.ui.activeView() + # Only add the Menu if Bins or Sequences are selected (this ensures menu isn't added in the Tags Pane) + if len(selection) > 0: + self._versionEverywhereMenu = self.createVersionEveryWhereMenuForView( + view) + hiero.ui.insertMenuAction( + self._versionEverywhereMenu.menuAction(), + event.menu, + after="foundry.menu.version") + return + + +# Instantiate the Menu to get it to register itself. +VersionAllMenu = VersionAllMenu() diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/PimpMySpreadsheet.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/PimpMySpreadsheet.py new file mode 100644 index 0000000000..3d40aa0293 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/PimpMySpreadsheet.py @@ -0,0 +1,844 @@ +# PimpMySpreadsheet 1.0, Antony Nasce, 23/05/13. +# Adds custom spreadsheet columns and right-click menu for setting the Shot Status, and Artist Shot Assignement. +# gStatusTags is a global dictionary of key(status)-value(icon) pairs, which can be overridden with custom icons if required +# Requires Hiero 1.7v2 or later. +# Install Instructions: Copy to ~/.hiero/Python/StartupUI + +import hiero.core +import hiero.ui + +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + +# Set to True, if you wat 'Set Status' right-click menu, False if not +kAddStatusMenu = True + +# Set to True, if you wat 'Assign Artist' right-click menu, False if not +kAssignArtistMenu = True + +# Global list of Artist Name Dictionaries +# Note: Override this to add different names, icons, department, IDs. +gArtistList = [{ + 'artistName': 'John Smith', + 'artistIcon': 'icons:TagActor.png', + 'artistDepartment': '3D', + 'artistID': 0 +}, { + 'artistName': 'Savlvador Dali', + 'artistIcon': 'icons:TagActor.png', + 'artistDepartment': 'Roto', + 'artistID': 1 +}, { + 'artistName': 'Leonardo Da Vinci', + 'artistIcon': 'icons:TagActor.png', + 'artistDepartment': 'Paint', + 'artistID': 2 +}, { + 'artistName': 'Claude Monet', + 'artistIcon': 'icons:TagActor.png', + 'artistDepartment': 'Comp', + 'artistID': 3 +}, { + 'artistName': 'Pablo Picasso', + 'artistIcon': 'icons:TagActor.png', + 'artistDepartment': 'Animation', + 'artistID': 4 +}] + +# Global Dictionary of Status Tags. +# Note: This can be overwritten if you want to add a new status cellType or custom icon +# Override the gStatusTags dictionary by adding your own 'Status':'Icon.png' key-value pairs. +# Add new custom keys like so: gStatusTags['For Client'] = 'forClient.png' +gStatusTags = { + 'Approved': 'icons:status/TagApproved.png', + 'Unapproved': 'icons:status/TagUnapproved.png', + 'Ready To Start': 'icons:status/TagReadyToStart.png', + 'Blocked': 'icons:status/TagBlocked.png', + 'On Hold': 'icons:status/TagOnHold.png', + 'In Progress': 'icons:status/TagInProgress.png', + 'Awaiting Approval': 'icons:status/TagAwaitingApproval.png', + 'Omitted': 'icons:status/TagOmitted.png', + 'Final': 'icons:status/TagFinal.png' +} + + +# The Custom Spreadsheet Columns +class CustomSpreadsheetColumns(QObject): + """ + A class defining custom columns for Hiero's spreadsheet view. This has a similar, but + slightly simplified, interface to the QAbstractItemModel and QItemDelegate classes. + """ + global gStatusTags + global gArtistList + + # Ideally, we'd set this list on a Per Item basis, but this is expensive for a large mixed selection + standardColourSpaces = [ + 'linear', 'sRGB', 'rec709', 'Cineon', 'Gamma1.8', 'Gamma2.2', + 'Panalog', 'REDLog', 'ViperLog' + ] + arriColourSpaces = [ + 'Video - Rec709', 'LogC - Camera Native', 'Video - P3', 'ACES', + 'LogC - Film', 'LogC - Wide Gamut' + ] + r3dColourSpaces = [ + 'Linear', 'Rec709', 'REDspace', 'REDlog', 'PDlog685', 'PDlog985', + 'CustomPDlog', 'REDgamma', 'SRGB', 'REDlogFilm', 'REDgamma2', + 'REDgamma3' + ] + gColourSpaces = standardColourSpaces + arriColourSpaces + r3dColourSpaces + + currentView = hiero.ui.activeView() + + # This is the list of Columns available + gCustomColumnList = [ + { + 'name': 'Tags', + 'cellType': 'readonly' + }, + { + 'name': 'Colourspace', + 'cellType': 'dropdown' + }, + { + 'name': 'Notes', + 'cellType': 'readonly' + }, + { + 'name': 'FileType', + 'cellType': 'readonly' + }, + { + 'name': 'Shot Status', + 'cellType': 'dropdown' + }, + { + 'name': 'Thumbnail', + 'cellType': 'readonly' + }, + { + 'name': 'MediaType', + 'cellType': 'readonly' + }, + { + 'name': 'Width', + 'cellType': 'readonly' + }, + { + 'name': 'Height', + 'cellType': 'readonly' + }, + { + 'name': 'Pixel Aspect', + 'cellType': 'readonly' + }, + { + 'name': 'Artist', + 'cellType': 'dropdown' + }, + { + 'name': 'Department', + 'cellType': 'readonly' + }, + ] + + def numColumns(self): + """ + Return the number of custom columns in the spreadsheet view + """ + return len(self.gCustomColumnList) + + def columnName(self, column): + """ + Return the name of a custom column + """ + return self.gCustomColumnList[column]['name'] + + def getTagsString(self, item): + """ + Convenience method for returning all the Notes in a Tag as a string + """ + tagNames = [] + tags = item.tags() + for tag in tags: + tagNames += [tag.name()] + tagNameString = ','.join(tagNames) + return tagNameString + + def getNotes(self, item): + """ + Convenience method for returning all the Notes in a Tag as a string + """ + notes = '' + tags = item.tags() + for tag in tags: + note = tag.note() + if len(note) > 0: + notes += tag.note() + ', ' + return notes[:-2] + + def getData(self, row, column, item): + """ + Return the data in a cell + """ + currentColumn = self.gCustomColumnList[column] + if currentColumn['name'] == 'Tags': + return self.getTagsString(item) + + if currentColumn['name'] == 'Colourspace': + try: + colTransform = item.sourceMediaColourTransform() + except: + colTransform = '--' + return colTransform + + if currentColumn['name'] == 'Notes': + try: + note = self.getNotes(item) + except: + note = '' + return note + + if currentColumn['name'] == 'FileType': + fileType = '--' + M = item.source().mediaSource().metadata() + if M.hasKey('foundry.source.type'): + fileType = M.value('foundry.source.type') + elif M.hasKey('media.input.filereader'): + fileType = M.value('media.input.filereader') + return fileType + + if currentColumn['name'] == 'Shot Status': + status = item.status() + if not status: + status = "--" + return str(status) + + if currentColumn['name'] == 'MediaType': + M = item.mediaType() + return str(M).split('MediaType')[-1].replace('.k', '') + + if currentColumn['name'] == 'Thumbnail': + return str(item.eventNumber()) + + if currentColumn['name'] == 'Width': + return str(item.source().format().width()) + + if currentColumn['name'] == 'Height': + return str(item.source().format().height()) + + if currentColumn['name'] == 'Pixel Aspect': + return str(item.source().format().pixelAspect()) + + if currentColumn['name'] == 'Artist': + if item.artist(): + name = item.artist()['artistName'] + return name + else: + return '--' + + if currentColumn['name'] == 'Department': + if item.artist(): + dep = item.artist()['artistDepartment'] + return dep + else: + return '--' + + return "" + + def setData(self, row, column, item, data): + """ + Set the data in a cell - unused in this example + """ + + return None + + def getTooltip(self, row, column, item): + """ + Return the tooltip for a cell + """ + currentColumn = self.gCustomColumnList[column] + if currentColumn['name'] == 'Tags': + return str([item.name() for item in item.tags()]) + + if currentColumn['name'] == 'Notes': + return str(self.getNotes(item)) + return "" + + def getFont(self, row, column, item): + """ + Return the tooltip for a cell + """ + return None + + def getBackground(self, row, column, item): + """ + Return the background colour for a cell + """ + if not item.source().mediaSource().isMediaPresent(): + return QColor(80, 20, 20) + return None + + def getForeground(self, row, column, item): + """ + Return the text colour for a cell + """ + #if column == 1: + # return QColor(255, 64, 64) + return None + + def getIcon(self, row, column, item): + """ + Return the icon for a cell + """ + currentColumn = self.gCustomColumnList[column] + if currentColumn['name'] == 'Colourspace': + return QIcon("icons:LUT.png") + + if currentColumn['name'] == 'Shot Status': + status = item.status() + if status: + return QIcon(gStatusTags[status]) + + if currentColumn['name'] == 'MediaType': + mediaType = item.mediaType() + if mediaType == hiero.core.TrackItem.kVideo: + return QIcon("icons:VideoOnly.png") + elif mediaType == hiero.core.TrackItem.kAudio: + return QIcon("icons:AudioOnly.png") + + if currentColumn['name'] == 'Artist': + try: + return QIcon(item.artist()['artistIcon']) + except: + return None + return None + + def getSizeHint(self, row, column, item): + """ + Return the size hint for a cell + """ + currentColumnName = self.gCustomColumnList[column]['name'] + + if currentColumnName == 'Thumbnail': + return QSize(90, 50) + + return QSize(50, 50) + + def paintCell(self, row, column, item, painter, option): + """ + Paint a custom cell. Return True if the cell was painted, or False to continue + with the default cell painting. + """ + currentColumn = self.gCustomColumnList[column] + if currentColumn['name'] == 'Tags': + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + iconSize = 20 + r = QRect(option.rect.x(), + option.rect.y() + (option.rect.height() - iconSize) / 2, + iconSize, iconSize) + tags = item.tags() + if len(tags) > 0: + painter.save() + painter.setClipRect(option.rect) + for tag in item.tags(): + M = tag.metadata() + if not (M.hasKey('tag.status') + or M.hasKey('tag.artistID')): + QIcon(tag.icon()).paint(painter, r, Qt.AlignLeft) + r.translate(r.width() + 2, 0) + painter.restore() + return True + + if currentColumn['name'] == 'Thumbnail': + imageView = None + pen = QPen() + r = QRect(option.rect.x() + 2, (option.rect.y() + + (option.rect.height() - 46) / 2), + 85, 46) + if not item.source().mediaSource().isMediaPresent(): + imageView = QImage("icons:Offline.png") + pen.setColor(QColor(Qt.red)) + + if item.mediaType() == hiero.core.TrackItem.MediaType.kAudio: + imageView = QImage("icons:AudioOnly.png") + #pen.setColor(QColor(Qt.green)) + painter.fillRect(r, QColor(45, 59, 45)) + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + + tags = item.tags() + painter.save() + painter.setClipRect(option.rect) + + if not imageView: + try: + imageView = item.thumbnail(item.sourceIn()) + pen.setColor(QColor(20, 20, 20)) + # If we're here, we probably have a TC error, no thumbnail, so get it from the source Clip... + except: + pen.setColor(QColor(Qt.red)) + + if not imageView: + try: + imageView = item.source().thumbnail() + pen.setColor(QColor(Qt.yellow)) + except: + imageView = QImage("icons:Offline.png") + pen.setColor(QColor(Qt.red)) + + QIcon(QPixmap.fromImage(imageView)).paint(painter, r, + Qt.AlignCenter) + painter.setPen(pen) + painter.drawRoundedRect(r, 1, 1) + painter.restore() + return True + + return False + + def createEditor(self, row, column, item, view): + """ + Create an editing widget for a custom cell + """ + self.currentView = view + + currentColumn = self.gCustomColumnList[column] + if currentColumn['cellType'] == 'readonly': + cle = QLabel() + cle.setEnabled(False) + cle.setVisible(False) + return cle + + if currentColumn['name'] == 'Colourspace': + cb = QComboBox() + for colourspace in self.gColourSpaces: + cb.addItem(colourspace) + cb.currentIndexChanged.connect(self.colourspaceChanged) + return cb + + if currentColumn['name'] == 'Shot Status': + cb = QComboBox() + cb.addItem('') + for key in gStatusTags.keys(): + cb.addItem(QIcon(gStatusTags[key]), key) + cb.addItem('--') + cb.currentIndexChanged.connect(self.statusChanged) + + return cb + + if currentColumn['name'] == 'Artist': + cb = QComboBox() + cb.addItem('') + for artist in gArtistList: + cb.addItem(artist['artistName']) + cb.addItem('--') + cb.currentIndexChanged.connect(self.artistNameChanged) + return cb + return None + + def setModelData(self, row, column, item, editor): + return False + + def dropMimeData(self, row, column, item, data, items): + """ + Handle a drag and drop operation - adds a Dragged Tag to the shot + """ + for thing in items: + if isinstance(thing, hiero.core.Tag): + item.addTag(thing) + return None + + def colourspaceChanged(self, index): + """ + This method is called when Colourspace widget changes index. + """ + index = self.sender().currentIndex() + colourspace = self.gColourSpaces[index] + selection = self.currentView.selection() + project = selection[0].project() + with project.beginUndo("Set Colourspace"): + items = [ + item for item in selection + if (item.mediaType() == hiero.core.TrackItem.MediaType.kVideo) + ] + for trackItem in items: + trackItem.setSourceMediaColourTransform(colourspace) + + def statusChanged(self, arg): + """ + This method is called when Shot Status widget changes index. + """ + view = hiero.ui.activeView() + selection = view.selection() + status = self.sender().currentText() + project = selection[0].project() + with project.beginUndo("Set Status"): + # A string of '--' characters denotes clear the status + if status != '--': + for trackItem in selection: + trackItem.setStatus(status) + else: + for trackItem in selection: + tTags = trackItem.tags() + for tag in tTags: + if tag.metadata().hasKey('tag.status'): + trackItem.removeTag(tag) + break + + def artistNameChanged(self, arg): + """ + This method is called when Artist widget changes index. + """ + view = hiero.ui.activeView() + selection = view.selection() + name = self.sender().currentText() + project = selection[0].project() + with project.beginUndo("Assign Artist"): + # A string of '--' denotes clear the assignee... + if name != '--': + for trackItem in selection: + trackItem.setArtistByName(name) + else: + for trackItem in selection: + tTags = trackItem.tags() + for tag in tTags: + if tag.metadata().hasKey('tag.artistID'): + trackItem.removeTag(tag) + break + + +def _getArtistFromID(self, artistID): + """ getArtistFromID -> returns an artist dictionary, by their given ID""" + global gArtistList + artist = [ + element for element in gArtistList + if element['artistID'] == int(artistID) + ] + if not artist: + return None + return artist[0] + + +def _getArtistFromName(self, artistName): + """ getArtistFromID -> returns an artist dictionary, by their given ID """ + global gArtistList + artist = [ + element for element in gArtistList + if element['artistName'] == artistName + ] + if not artist: + return None + return artist[0] + + +def _artist(self): + """_artist -> Returns the artist dictionary assigned to this shot""" + artist = None + tags = self.tags() + for tag in tags: + if tag.metadata().hasKey('tag.artistID'): + artistID = tag.metadata().value('tag.artistID') + artist = self.getArtistFromID(artistID) + return artist + + +def _updateArtistTag(self, artistDict): + # A shot will only have one artist assigned. Check if one exists and set accordingly + + artistTag = None + tags = self.tags() + for tag in tags: + if tag.metadata().hasKey('tag.artistID'): + artistTag = tag + break + + if not artistTag: + artistTag = hiero.core.Tag('Artist') + artistTag.setIcon(artistDict['artistIcon']) + artistTag.metadata().setValue('tag.artistID', + str(artistDict['artistID'])) + artistTag.metadata().setValue('tag.artistName', + str(artistDict['artistName'])) + artistTag.metadata().setValue('tag.artistDepartment', + str(artistDict['artistDepartment'])) + self.sequence().editFinished() + self.addTag(artistTag) + self.sequence().editFinished() + return + + artistTag.setIcon(artistDict['artistIcon']) + artistTag.metadata().setValue('tag.artistID', str(artistDict['artistID'])) + artistTag.metadata().setValue('tag.artistName', + str(artistDict['artistName'])) + artistTag.metadata().setValue('tag.artistDepartment', + str(artistDict['artistDepartment'])) + self.sequence().editFinished() + return + + +def _setArtistByName(self, artistName): + """ setArtistByName(artistName) -> sets the artist tag on a TrackItem by a given artistName string""" + global gArtistList + + artist = self.getArtistFromName(artistName) + if not artist: + print 'Artist name: %s was not found in the gArtistList.' % str( + artistName) + return + + # Do the update. + self.updateArtistTag(artist) + + +def _setArtistByID(self, artistID): + """ setArtistByID(artistID) -> sets the artist tag on a TrackItem by a given artistID integer""" + global gArtistList + + artist = self.getArtistFromID(artistID) + if not artist: + print 'Artist name: %s was not found in the gArtistList.' % str( + artistID) + return + + # Do the update. + self.updateArtistTag(artist) + + +# Inject status getter and setter methods into hiero.core.TrackItem +hiero.core.TrackItem.artist = _artist +hiero.core.TrackItem.setArtistByName = _setArtistByName +hiero.core.TrackItem.setArtistByID = _setArtistByID +hiero.core.TrackItem.getArtistFromName = _getArtistFromName +hiero.core.TrackItem.getArtistFromID = _getArtistFromID +hiero.core.TrackItem.updateArtistTag = _updateArtistTag + + +def _status(self): + """status -> Returns the Shot status. None if no Status is set.""" + + status = None + tags = self.tags() + for tag in tags: + if tag.metadata().hasKey('tag.status'): + status = tag.metadata().value('tag.status') + return status + + +def _setStatus(self, status): + """setShotStatus(status) -> Method to set the Status of a Shot. + Adds a special kind of status Tag to a TrackItem + Example: myTrackItem.setStatus('Final') + + @param status - a string, corresponding to the Status name + """ + global gStatusTags + + # Get a valid Tag object from the Global list of statuses + if not status in gStatusTags.keys(): + print 'Status requested was not a valid Status string.' + return + + # A shot should only have one status. Check if one exists and set accordingly + statusTag = None + tags = self.tags() + for tag in tags: + if tag.metadata().hasKey('tag.status'): + statusTag = tag + break + + if not statusTag: + statusTag = hiero.core.Tag('Status') + statusTag.setIcon(gStatusTags[status]) + statusTag.metadata().setValue('tag.status', status) + self.addTag(statusTag) + + statusTag.setIcon(gStatusTags[status]) + statusTag.metadata().setValue('tag.status', status) + + self.sequence().editFinished() + return + + +# Inject status getter and setter methods into hiero.core.TrackItem +hiero.core.TrackItem.setStatus = _setStatus +hiero.core.TrackItem.status = _status + + +# This is a convenience method for returning QActions with a triggered method based on the title string +def titleStringTriggeredAction(title, method, icon=None): + action = QAction(title, None) + action.setIcon(QIcon(icon)) + + # We do this magic, so that the title string from the action is used to set the status + def methodWrapper(): + method(title) + + action.triggered.connect(methodWrapper) + return action + + +# Menu which adds a Set Status Menu to Timeline and Spreadsheet Views +class SetStatusMenu(QMenu): + def __init__(self): + QMenu.__init__(self, "Set Status", None) + + global gStatusTags + self.statuses = gStatusTags + self._statusActions = self.createStatusMenuActions() + + # Add the Actions to the Menu. + for act in self.menuActions: + self.addAction(act) + + hiero.core.events.registerInterest("kShowContextMenu/kTimeline", + self.eventHandler) + hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet", + self.eventHandler) + + def createStatusMenuActions(self): + self.menuActions = [] + for status in self.statuses: + self.menuActions += [ + titleStringTriggeredAction( + status, + self.setStatusFromMenuSelection, + icon=gStatusTags[status]) + ] + + def setStatusFromMenuSelection(self, menuSelectionStatus): + selectedShots = [ + item for item in self._selection + if (isinstance(item, hiero.core.TrackItem)) + ] + selectedTracks = [ + item for item in self._selection + if (isinstance(item, (hiero.core.VideoTrack, + hiero.core.AudioTrack))) + ] + + # If we have a Track Header Selection, no shots could be selected, so create shotSelection list + if len(selectedTracks) >= 1: + for track in selectedTracks: + selectedShots += [ + item for item in track.items() + if (isinstance(item, hiero.core.TrackItem)) + ] + + # It's possible no shots exist on the Track, in which case nothing is required + if len(selectedShots) == 0: + return + + currentProject = selectedShots[0].project() + + with currentProject.beginUndo("Set Status"): + # Shots selected + for shot in selectedShots: + shot.setStatus(menuSelectionStatus) + + # This handles events from the Project Bin View + def eventHandler(self, event): + if not hasattr(event.sender, 'selection'): + # Something has gone wrong, we should only be here if raised + # by the Timeline/Spreadsheet view which gives a selection. + return + + # Set the current selection + self._selection = event.sender.selection() + + # Return if there's no Selection. We won't add the Menu. + if len(self._selection) == 0: + return + + event.menu.addMenu(self) + + +# Menu which adds a Set Status Menu to Timeline and Spreadsheet Views +class AssignArtistMenu(QMenu): + def __init__(self): + QMenu.__init__(self, "Assign Artist", None) + + global gArtistList + self.artists = gArtistList + self._artistsActions = self.createAssignArtistMenuActions() + + # Add the Actions to the Menu. + for act in self.menuActions: + self.addAction(act) + + hiero.core.events.registerInterest("kShowContextMenu/kTimeline", + self.eventHandler) + hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet", + self.eventHandler) + + def createAssignArtistMenuActions(self): + self.menuActions = [] + for artist in self.artists: + self.menuActions += [ + titleStringTriggeredAction( + artist['artistName'], + self.setArtistFromMenuSelection, + icon=artist['artistIcon']) + ] + + def setArtistFromMenuSelection(self, menuSelectionArtist): + selectedShots = [ + item for item in self._selection + if (isinstance(item, hiero.core.TrackItem)) + ] + selectedTracks = [ + item for item in self._selection + if (isinstance(item, (hiero.core.VideoTrack, + hiero.core.AudioTrack))) + ] + + # If we have a Track Header Selection, no shots could be selected, so create shotSelection list + if len(selectedTracks) >= 1: + for track in selectedTracks: + selectedShots += [ + item for item in track.items() + if (isinstance(item, hiero.core.TrackItem)) + ] + + # It's possible no shots exist on the Track, in which case nothing is required + if len(selectedShots) == 0: + return + + currentProject = selectedShots[0].project() + + with currentProject.beginUndo("Assign Artist"): + # Shots selected + for shot in selectedShots: + shot.setArtistByName(menuSelectionArtist) + + # This handles events from the Project Bin View + def eventHandler(self, event): + if not hasattr(event.sender, 'selection'): + # Something has gone wrong, we should only be here if raised + # by the Timeline/Spreadsheet view which gives a selection. + return + + # Set the current selection + self._selection = event.sender.selection() + + # Return if there's no Selection. We won't add the Menu. + if len(self._selection) == 0: + return + + event.menu.addMenu(self) + + +# Add the 'Set Status' context menu to Timeline and Spreadsheet +if kAddStatusMenu: + setStatusMenu = SetStatusMenu() + +if kAssignArtistMenu: + assignArtistMenu = AssignArtistMenu() + +# Register our custom columns +hiero.ui.customColumn = CustomSpreadsheetColumns() diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/Purge.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/Purge.py new file mode 100644 index 0000000000..4d2ab255ad --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/Purge.py @@ -0,0 +1,142 @@ +# Purge Unused Clips - Removes any unused Clips from a Project +# Usage: Copy to ~/.hiero/Python/StartupUI +# Demonstrates the use of hiero.core.find_items module. +# Usage: Right-click on an item in the Bin View > "Purge Unused Clips" +# Result: Any Clips not used in a Sequence in the active project will be removed +# Requires Hiero 1.5v1 or later. +# Version 1.1 + +import hiero +import hiero.core.find_items +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + + +class PurgeUnusedAction(QAction): + def __init__(self): + QAction.__init__(self, "Purge Unused Clips", None) + self.triggered.connect(self.PurgeUnused) + hiero.core.events.registerInterest("kShowContextMenu/kBin", + self.eventHandler) + self.setIcon(QIcon('icons:TagDelete.png')) + + # Method to return whether a Bin is empty... + def binIsEmpty(self, b): + numBinItems = 0 + bItems = b.items() + empty = False + + if len(bItems) == 0: + empty = True + return empty + else: + for b in bItems: + if isinstance(b, hiero.core.BinItem) or isinstance( + b, hiero.core.Bin): + numBinItems += 1 + if numBinItems == 0: + empty = True + + return empty + + def PurgeUnused(self): + + #Get selected items + item = self.selectedItem + proj = item.project() + + # Build a list of Projects + SEQS = hiero.core.findItems(proj, "Sequences") + + # Build a list of Clips + CLIPSTOREMOVE = hiero.core.findItems(proj, "Clips") + + if len(SEQS) == 0: + # Present Dialog Asking if User wants to remove Clips + msgBox = QMessageBox() + msgBox.setText("Purge Unused Clips") + msgBox.setInformativeText( + "You have no Sequences in this Project. Do you want to remove all Clips (%i) from Project: %s?" + % (len(CLIPSTOREMOVE), proj.name())) + msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) + msgBox.setDefaultButton(QMessageBox.Ok) + ret = msgBox.exec_() + if ret == QMessageBox.Cancel: + print 'Not purging anything.' + elif ret == QMessageBox.Ok: + with proj.beginUndo('Purge Unused Clips'): + BINS = [] + for clip in CLIPSTOREMOVE: + BI = clip.binItem() + B = BI.parentBin() + BINS += [B] + print 'Removing:', BI + try: + B.removeItem(BI) + except: + print 'Unable to remove: ' + BI + return + + # For each sequence, iterate through each track Item, see if the Clip is in the CLIPS list. + # Remaining items in CLIPS will be removed + + for seq in SEQS: + + #Loop through selected and make folders + for track in seq: + for trackitem in track: + + if trackitem.source() in CLIPSTOREMOVE: + CLIPSTOREMOVE.remove(trackitem.source()) + + # Present Dialog Asking if User wants to remove Clips + msgBox = QMessageBox() + msgBox.setText("Purge Unused Clips") + msgBox.setInformativeText("Remove %i unused Clips from Project %s?" % + (len(CLIPSTOREMOVE), proj.name())) + msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) + msgBox.setDefaultButton(QMessageBox.Ok) + ret = msgBox.exec_() + + if ret == QMessageBox.Cancel: + print 'Cancel' + return + elif ret == QMessageBox.Ok: + BINS = [] + with proj.beginUndo('Purge Unused Clips'): + # Delete the rest of the Clips + for clip in CLIPSTOREMOVE: + BI = clip.binItem() + B = BI.parentBin() + BINS += [B] + print 'Removing:', BI + try: + B.removeItem(BI) + except: + print 'Unable to remove: ' + BI + + def eventHandler(self, event): + if not hasattr(event.sender, 'selection'): + # Something has gone wrong, we shouldn't only be here if raised + # by the Bin view which will give a selection. + return + + self.selectedItem = None + s = event.sender.selection() + + if len(s) >= 1: + self.selectedItem = s[0] + title = "Purge Unused Clips" + self.setText(title) + event.menu.addAction(self) + + return + + +# Instantiate the action to get it to register itself. +PurgeUnusedAction = PurgeUnusedAction() diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py new file mode 100644 index 0000000000..36a30e3a3c --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py @@ -0,0 +1,36 @@ +# nukeStyleKeyboardShortcuts, v1, 30/07/2012, Ant Nasce. +# A few Nuke-Style File menu shortcuts for those whose muscle memory has set in... +# Usage: Copy this file to ~/.hiero/Python/StartupUI/ + +import hiero.ui +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + +#---------------------------------------------- +a = hiero.ui.findMenuAction('Import Clips...') +# Note: You probably best to make this 'Ctrl+R' - currently conflicts with 'Red' in the Viewer! +a.setShortcut(QKeySequence('R')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('Import Folder...') +a.setShortcut(QKeySequence('Shift+R')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('Import EDL/XML...') +a.setShortcut(QKeySequence('Ctrl+Shift+O')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('Show Metadata') +a.setShortcut(QKeySequence('I')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('Edit Settings') +a.setShortcut(QKeySequence('S')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('Monitor Controls') +a.setShortcut(QKeySequence('Ctrl+U')) +#---------------------------------------------- +a = hiero.ui.findMenuAction('New Viewer') +a.setShortcut(QKeySequence('Ctrl+I')) +#---------------------------------------------- diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/setPosterFrame.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/setPosterFrame.py new file mode 100644 index 0000000000..18398aa119 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/setPosterFrame.py @@ -0,0 +1,45 @@ +import hiero.core +import hiero.ui +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + + +def setPosterFrame(posterFrame=.5): + ''' + Update the poster frame of the given clipItmes + posterFrame = .5 uses the centre frame, a value of 0 uses the first frame, a value of 1 uses the last frame + ''' + view = hiero.ui.activeView() + + selectedBinItems = view.selection() + selectedClipItems = [(item.activeItem() + if hasattr(item, 'activeItem') else item) + for item in selectedBinItems] + + for clip in selectedClipItems: + centreFrame = int(clip.duration() * posterFrame) + clip.setPosterFrame(centreFrame) + + +class SetPosterFrameAction(QAction): + def __init__(self): + QAction.__init__(self, "Set Poster Frame (centre)", None) + self._selection = None + + self.triggered.connect(lambda: setPosterFrame(.5)) + hiero.core.events.registerInterest("kShowContextMenu/kBin", + self.eventHandler) + + def eventHandler(self, event): + view = event.sender + # Add the Menu to the right-click menu + event.menu.addAction(self) + + +# The act of initialising the action adds it to the right-click menu... +SetPosterFrameAction() diff --git a/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py b/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py new file mode 100644 index 0000000000..4459be6713 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py @@ -0,0 +1,14 @@ +import traceback + +try: + __import__("pype.nukestudio") + __import__("pyblish") + +except ImportError as e: + print traceback.format_exc() + print("pyblish: Could not load integration: %s " % e) + +else: + # Setup integration + import pype.nukestudio.lib + pype.nukestudio.lib.setup() diff --git a/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py b/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py new file mode 100644 index 0000000000..b7e05fed7c --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py @@ -0,0 +1,9 @@ +"""Puts the selection project into 'hiero.selection'""" + +import hiero + + +def selectionChanged(event): + hiero.selection = event.sender.selection() + +hiero.core.events.registerInterest('kSelectionChanged', selectionChanged) diff --git a/setup/nukestudio/hiero_plugin_path/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/setup/nukestudio/hiero_plugin_path/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml new file mode 100644 index 0000000000..e24a4dbe4e --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml @@ -0,0 +1,198 @@ + + 991 + //10.11.0.184/171001_ftrack/tgbvfx/editorial/nukestudio/workspace/ + 1 + True + 3 + + + {shot}/editorial_raw.%04d.{fileext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + False + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 32 bit float + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.%04d.{ext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + True + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 16 bit half + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + To Sequence Resolution + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.nk + + + True + default + mov + + rgb + False + + False + False + False + + True + True + + {shot}/editorial_raw.%04d.{fileext} + + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend + False + True + True + + 0 + 40000000 + 12 + 31 + 2 + avc1 H.264 + Auto + mov32 + 20000 + + False + True + True + False + False + {shot}/editorial_raw.%04d.{fileext} + + None + None + None + None + None + None + None + None + None + + + 8 bit + (auto detect) + True + False + + Write_{ext} + False +
+
+
+
+ + + False + Custom + True + 10 +
diff --git a/setup/nukestudio/hiero_plugin_path/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/setup/nukestudio/hiero_plugin_path/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml new file mode 100644 index 0000000000..e24a4dbe4e --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml @@ -0,0 +1,198 @@ + + 991 + //10.11.0.184/171001_ftrack/tgbvfx/editorial/nukestudio/workspace/ + 1 + True + 3 + + + {shot}/editorial_raw.%04d.{fileext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + False + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 32 bit float + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.%04d.{ext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + True + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 16 bit half + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + To Sequence Resolution + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.nk + + + True + default + mov + + rgb + False + + False + False + False + + True + True + + {shot}/editorial_raw.%04d.{fileext} + + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend + False + True + True + + 0 + 40000000 + 12 + 31 + 2 + avc1 H.264 + Auto + mov32 + 20000 + + False + True + True + False + False + {shot}/editorial_raw.%04d.{fileext} + + None + None + None + None + None + None + None + None + None + + + 8 bit + (auto detect) + True + False + + Write_{ext} + False +
+
+
+
+ + + False + Custom + True + 10 +
diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox new file mode 100644 index 0000000000..684cd0d1a2 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox @@ -0,0 +1,38 @@ + + + + + + + + + 2 + 70 + + + 2 + 70 + + + 2 + 70 + 13 + + + 2 + 70 + 17 + + + 2 + 70 + 2 + + + + + + + + + diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox new file mode 100644 index 0000000000..e915a24084 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox @@ -0,0 +1,38 @@ + + + + + + + + + 2 + 70 + + + 2 + 70 + + + 2 + 70 + 13 + + + 2 + 70 + 17 + + + 2 + 70 + 2 + + + + + + + + + diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox new file mode 100644 index 0000000000..42659cf81b --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox @@ -0,0 +1,38 @@ + + + + + + + + + 2 + 70 + + + 2 + 70 + + + 2 + 70 + 13 + + + 2 + 70 + 17 + + + 2 + 70 + 2 + + + + + + + + + From 6b443ae7a40da093c22f8aa500333c92358d8bd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 Apr 2019 13:28:17 +0200 Subject: [PATCH 113/193] fix(nukestudio): adding and altering more initial stuf --- pype/nukestudio/__init__.py | 47 +---- .../{pyblish_startup.py => Startup.py} | 5 + .../Startup_old/pyblish_startup.py | 14 -- .../Startup_old/selection_tracker.py | 9 - .../pipeline.xml | 198 ++++++++++++++++++ 5 files changed, 211 insertions(+), 62 deletions(-) rename setup/nukestudio/hiero_plugin_path/Python/Startup/{pyblish_startup.py => Startup.py} (72%) delete mode 100644 setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py delete mode 100644 setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py create mode 100644 setup/nukestudio/hiero_plugin_path/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py index f3ef69608f..7a87009141 100644 --- a/pype/nukestudio/__init__.py +++ b/pype/nukestudio/__init__.py @@ -32,8 +32,6 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "inventory") -self = sys.modules[__name__] -self.nLogger = None if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) @@ -49,14 +47,12 @@ def reload_config(): import importlib for module in ( - "app", - "app.api", + "pypeapp", "{}.api".format(AVALON_CONFIG), "{}.templates".format(AVALON_CONFIG), - "{}.nuke.actions".format(AVALON_CONFIG), - "{}.nuke.templates".format(AVALON_CONFIG), - "{}.nuke.menu".format(AVALON_CONFIG), - "{}.nuke.lib".format(AVALON_CONFIG), + "{}.nukestudio.inventory".format(AVALON_CONFIG), + "{}.nukestudio.lib".format(AVALON_CONFIG), + "{}.nukestudio.menu".format(AVALON_CONFIG), ): log.info("Reloading module: {}...".format(module)) try: @@ -74,9 +70,9 @@ def install(): import sys - for path in sys.path: - if path.startswith("C:\\Users\\Public"): - sys.path.remove(path) + # for path in sys.path: + # if path.startswith("C:\\Users\\Public"): + # sys.path.remove(path) log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) @@ -84,8 +80,6 @@ def install(): avalon.register_plugin_path(avalon.Creator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) - # Disable all families except for the ones we explicitly want to see family_states = [ "write", @@ -98,7 +92,7 @@ def install(): menu.install() # load data from templates - # api.load_data_from_templates() + api.load_data_from_templates() def uninstall(): @@ -107,30 +101,5 @@ def uninstall(): avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) - pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) - # reset data from templates api.reset_data_from_templates() - - -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle node passthrough states on instance toggles.""" - self.log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( - instance, old_value, new_value)) - - from avalon.nuke import ( - viewer_update_and_undo_stop, - add_publish_knob - ) - - # Whether instances should be passthrough based on new value - - with viewer_update_and_undo_stop(): - n = instance[0] - try: - n["publish"].value() - except ValueError: - n = add_publish_knob(n) - log.info(" `Publish` knob was added to write node..") - - n["publish"].setValue(new_value) diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/Startup.py similarity index 72% rename from setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py rename to setup/nukestudio/hiero_plugin_path/Python/Startup/Startup.py index 4459be6713..bbef6502a9 100644 --- a/setup/nukestudio/hiero_plugin_path/Python/Startup/pyblish_startup.py +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/Startup.py @@ -1,5 +1,10 @@ import traceback +# activate nukestudio from pype +import avalon.api +import pype.nukestudio +avalon.api.install(pype.nukestudio) + try: __import__("pype.nukestudio") __import__("pyblish") diff --git a/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py b/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py deleted file mode 100644 index 4459be6713..0000000000 --- a/setup/nukestudio/hiero_plugin_path/Startup_old/pyblish_startup.py +++ /dev/null @@ -1,14 +0,0 @@ -import traceback - -try: - __import__("pype.nukestudio") - __import__("pyblish") - -except ImportError as e: - print traceback.format_exc() - print("pyblish: Could not load integration: %s " % e) - -else: - # Setup integration - import pype.nukestudio.lib - pype.nukestudio.lib.setup() diff --git a/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py b/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py deleted file mode 100644 index b7e05fed7c..0000000000 --- a/setup/nukestudio/hiero_plugin_path/Startup_old/selection_tracker.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Puts the selection project into 'hiero.selection'""" - -import hiero - - -def selectionChanged(event): - hiero.selection = event.sender.selection() - -hiero.core.events.registerInterest('kSelectionChanged', selectionChanged) diff --git a/setup/nukestudio/hiero_plugin_path/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/setup/nukestudio/hiero_plugin_path/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml new file mode 100644 index 0000000000..e24a4dbe4e --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml @@ -0,0 +1,198 @@ + + 991 + //10.11.0.184/171001_ftrack/tgbvfx/editorial/nukestudio/workspace/ + 1 + True + 3 + + + {shot}/editorial_raw.%04d.{fileext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + False + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 32 bit float + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.%04d.{ext} + + + default + exr + False + all + False + False + False + False + True + + + 8 bit + (auto detect) + True + False + + True + + None + None + None + None + None + None + None + None + None + + + Zip (16 scanline) + 16 bit half + False + False + False + channels, layers and views + 45.0 + False + all metadata + + Write_{ext} + + Cubic + To Sequence Resolution + 1.0 +
True
+ width +
+ False + Blend +
+
+
+ + {shot}/editorial.nk + + + True + default + mov + + rgb + False + + False + False + False + + True + True + + {shot}/editorial_raw.%04d.{fileext} + + + Cubic + None + 1.0 +
True
+ width +
+ False + Blend + False + True + True + + 0 + 40000000 + 12 + 31 + 2 + avc1 H.264 + Auto + mov32 + 20000 + + False + True + True + False + False + {shot}/editorial_raw.%04d.{fileext} + + None + None + None + None + None + None + None + None + None + + + 8 bit + (auto detect) + True + False + + Write_{ext} + False +
+
+
+
+ + + False + Custom + True + 10 +
From c0742cffaf78f9d9405c61e10bb1944e2a1b1b80 Mon Sep 17 00:00:00 2001 From: jezschaj Date: Thu, 25 Apr 2019 15:33:52 +0200 Subject: [PATCH 114/193] fix(nuke): loading Anatomy and Colorspace, Dataflow with pype2 way --- pype/nuke/lib.py | 33 ++++++++++++++++++--------------- pype/nuke/templates.py | 14 ++++++++------ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index cc1ff5bfa7..46b1d6e4c8 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -58,7 +58,8 @@ def writes_version_sync(): node_new_file = node_file.replace(node_version, new_version) each['file'].setValue(node_new_file) except Exception as e: - log.debug("Write node: `{}` has no version in path: {}".format(each.name(), e)) + log.debug( + "Write node: `{}` has no version in path: {}".format(each.name(), e)) def version_up_script(): @@ -74,7 +75,7 @@ def get_render_path(node): data_preset = { "class": data['avalon']['family'], "preset": data['avalon']['families'] - } + } nuke_dataflow_writes = get_dataflow(**data_preset) nuke_colorspace_writes = get_colorspace(**data_preset) @@ -87,7 +88,8 @@ def get_render_path(node): }) anatomy_filled = format_anatomy(data) - return anatomy_filled.render.path.replace("\\", "/") + return anatomy_filled["render"]["path"].replace("\\", "/") + def format_anatomy(data): from .templates import ( @@ -95,28 +97,29 @@ def format_anatomy(data): ) anatomy = get_anatomy() - + log.info("__ anatomy.templates: {}".format(anatomy.templates)) # TODO: perhaps should be in try! - padding = anatomy.render.padding + padding = int(anatomy.templates['render']['padding']) version = data.get("version", None) if not version: file = script_name() data["version"] = pype.get_version_from_path(file) data.update({ + "root": api.Session["AVALON_PROJECTS"], "subset": data["avalon"]["subset"], "asset": data["avalon"]["asset"], "task": str(pype.get_task()).lower(), "family": data["avalon"]["family"], "project": {"name": pype.get_project_name(), "code": pype.get_project_code()}, - "representation": data["nuke_dataflow_writes"].file_type, + "representation": data["nuke_dataflow_writes"]["file_type"], "app": data["application"]["application_dir"], "hierarchy": pype.get_hierarchy(), "frame": "#" * padding, }) - - # log.info("format_anatomy:anatomy: {}".format(anatomy)) + log.info("__ data: {}".format(data)) + log.info("__ format_anatomy: {}".format(anatomy.format(data))) return anatomy.format(data) @@ -141,10 +144,8 @@ def create_write_node(name, data): except Exception as e: log.error("problem with resolving anatomy tepmlate: {}".format(e)) - log.debug("anatomy_filled.render: {}".format(anatomy_filled.render)) - _data = OrderedDict({ - "file": str(anatomy_filled.render.path).replace("\\", "/") + "file": str(anatomy_filled["render"]["path"]).replace("\\", "/") }) # adding dataflow template @@ -161,7 +162,7 @@ def create_write_node(name, data): log.debug(_data) _data["frame_range"] = data.get("frame_range", None) - + log.info("__ _data3: {}".format(_data)) instance = avalon.nuke.lib.add_write_node( name, **_data @@ -170,6 +171,7 @@ def create_write_node(name, data): add_rendering_knobs(instance) return instance + def add_rendering_knobs(node): if "render" not in node.knobs(): knob = nuke.Boolean_Knob("render", "Render") @@ -232,7 +234,8 @@ def set_root_colorspace(root_dict): # first set OCIO if nuke.root()["colorManagement"].value() not in str(root_dict["colorManagement"]): - nuke.root()["colorManagement"].setValue(str(root_dict["colorManagement"])) + nuke.root()["colorManagement"].setValue( + str(root_dict["colorManagement"])) # second set ocio version if nuke.root()["OCIO_config"].value() not in str(root_dict["OCIO_config"]): @@ -332,7 +335,7 @@ def reset_resolution(): check_format = used_formats[-1] format_name = "{}_{}".format( project["name"], - int(used_formats[-1].name()[-1])+1 + int(used_formats[-1].name()[-1]) + 1 ) log.info( "Format exists: {}. " @@ -458,7 +461,7 @@ def get_write_node_template_attr(node): data_preset = { "class": data['avalon']['family'], "preset": data['avalon']['families'] - } + } # get template data nuke_dataflow_writes = get_dataflow(**data_preset) diff --git a/pype/nuke/templates.py b/pype/nuke/templates.py index 4be350cbcb..b3de6970d0 100644 --- a/pype/nuke/templates.py +++ b/pype/nuke/templates.py @@ -15,10 +15,12 @@ def get_dataflow(**kwarg): assert any([host, cls]), log.error("nuke.templates.get_dataflow():" "Missing mandatory kwargs `host`, `cls`") - nuke_dataflow = getattr(pype.Dataflow, str(host), None) - nuke_dataflow_node = getattr(nuke_dataflow.nodes, str(cls), None) + nuke_dataflow = pype.Dataflow.get(str(host), None) + nuke_dataflow_nodes = nuke_dataflow.get('nodes', None) + nuke_dataflow_node = nuke_dataflow_nodes.get(str(cls), None) + if preset: - nuke_dataflow_node = getattr(nuke_dataflow_node, str(preset), None) + nuke_dataflow_node = nuke_dataflow_node.get(str(preset), None) log.info("Dataflow: {}".format(nuke_dataflow_node)) return nuke_dataflow_node @@ -32,10 +34,10 @@ def get_colorspace(**kwarg): assert any([host, cls]), log.error("nuke.templates.get_colorspace():" "Missing mandatory kwargs `host`, `cls`") - nuke_colorspace = getattr(pype.Colorspace, str(host), None) - nuke_colorspace_node = getattr(nuke_colorspace, str(cls), None) + nuke_colorspace = pype.Colorspace.get(str(host), None) + nuke_colorspace_node = nuke_colorspace.get(str(cls), None) if preset: - nuke_colorspace_node = getattr(nuke_colorspace_node, str(preset), None) + nuke_colorspace_node = nuke_colorspace_node.get(str(preset), None) log.info("Colorspace: {}".format(nuke_colorspace_node)) return nuke_colorspace_node From e7f21ccdca2b93a75f16286d7f5ac04db9a3e24d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 25 Apr 2019 22:34:45 +0100 Subject: [PATCH 115/193] Removing plugin loading of "spore". --- pype/maya/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 6b971c8bca..857f870f0d 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -107,9 +107,6 @@ def on_init(_): # Force load objExport plug-in (requested by artists) cmds.loadPlugin("objExport", quiet=True) - # Force load objExport plug-in (requested by artists) - cmds.loadPlugin("spore", quiet=True) - from .customize import ( override_component_mask_commands, override_toolbox_ui From 2bc1f67fedcb991bb97032fd412b6cafa7e6c7e4 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 25 Apr 2019 22:41:27 +0100 Subject: [PATCH 116/193] Launch workfiles app on Maya launch. --- pype/maya/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 6b971c8bca..0c00efb8ec 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -6,6 +6,7 @@ from maya import utils, cmds, mel from avalon import api as avalon, pipeline, maya from avalon.maya.pipeline import IS_HEADLESS +from avalon.tools import workfiles from pyblish import api as pyblish from ..lib import ( @@ -115,11 +116,21 @@ def on_init(_): override_toolbox_ui ) safe_deferred(override_component_mask_commands) + safe_deferred(launch_workfiles_app) if not IS_HEADLESS: safe_deferred(override_toolbox_ui) +def launch_workfiles_app(*args): + workfiles.show( + os.path.join( + cmds.workspace(query=True, rootDirectory=True), + cmds.workspace(fileRuleEntry="scene") + ) + ) + + def on_before_save(return_code, _): """Run validation for scene's FPS prior to saving""" return lib.validate_fps() From d1f72a425d5d8fd081a90dba6de4bbd4891f698c Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 26 Apr 2019 10:04:50 +0100 Subject: [PATCH 117/193] Making the collection of destination templates work on all instances. --- .../global/publish/collect_assumed_destination.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 7458db6aa7..fa6a3d9423 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -4,14 +4,18 @@ import pyblish.api from avalon import io, api -class CollectAssumedDestination(pyblish.api.InstancePlugin): +class CollectAssumedDestination(pyblish.api.ContextPlugin): """Generate the assumed destination path where the file will be stored""" label = "Collect Assumed Destination" order = pyblish.api.CollectorOrder + 0.498 exclude_families = ["clip"] - def process(self, instance): + def process(self, context): + for instance in context: + self.process_item(instance) + + def process_item(self, instance): if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return @@ -19,7 +23,6 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): self.create_destination_template(instance) template_data = instance.data["assumedTemplateData"] - template = instance.data["template"] anatomy = instance.context.data['anatomy'] # self.log.info(anatomy.anatomy()) From 33d926f383c1078da589516ee2cfb0200a0d486a Mon Sep 17 00:00:00 2001 From: jezschaj Date: Fri, 26 Apr 2019 12:38:28 +0200 Subject: [PATCH 118/193] fix(ftrack): integrate instance was broken.. --- .../plugins/ftrack/publish/integrate_ftrack_instances.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 958a14f4f4..75d9b6db15 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -27,8 +27,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 'nukescript': 'comp', 'review': 'mov'} + def process(self, instance): self.log.debug('instance {}'.format(instance)) - assumed_data = instance.data["assumedTemplateData"] assumed_version = assumed_data["version"] version_number = int(assumed_version) @@ -54,8 +54,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): self.log.debug('dest ext: ' + ext) thumbnail = False - - if ext in ['.mov']: if not instance.data.get('startFrameReview'): instance.data['startFrameReview'] = instance.data['startFrame'] @@ -64,12 +62,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): location = ft_session.query( 'Location where name is "ftrack.server"').one() component_data = { - "name": "ftrackreview-mp4", # Default component name is "main". + # Default component name is "main". + "name": "ftrackreview-mp4", "metadata": {'ftr_meta': json.dumps({ 'frameIn': int(instance.data['startFrameReview']), 'frameOut': int(instance.data['startFrameReview']), 'frameRate': 25})} - } + } elif ext in [".jpg", ".jpeg"]: component_data = { "name": "thumbnail" # Default component name is "main". From 942dd770fdef40b373ab6f8c9ab1cb066ac55020 Mon Sep 17 00:00:00 2001 From: antirotor Date: Fri, 26 Apr 2019 12:56:22 +0200 Subject: [PATCH 119/193] fix(setup): fixed PYPE_SETUP_ROOT -> PYPE_ROOT --- pype/aport/api.py | 2 +- pype/aport/original/api.py | 2 +- pype/aport/original/pipeline.py | 2 +- pype/clockify/widget_settings.py | 2 +- pype/ftrack/login_dialog.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/aport/api.py b/pype/aport/api.py index 42a090dc63..bac3e235df 100644 --- a/pype/aport/api.py +++ b/pype/aport/api.py @@ -56,7 +56,7 @@ def publish(json_data_path, gui): log.info("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") publish = "--publish-gui" if gui else "--publish" diff --git a/pype/aport/original/api.py b/pype/aport/original/api.py index b7ae447546..b1fffed1dc 100644 --- a/pype/aport/original/api.py +++ b/pype/aport/original/api.py @@ -56,7 +56,7 @@ def publish(json_data_path, staging_dir=None): return_json_path = os.path.join(staging_dir, "return_data.json") log.info("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") args = [pype_start, "--publish", diff --git a/pype/aport/original/pipeline.py b/pype/aport/original/pipeline.py index 2c37695225..1bfd9a8d1e 100644 --- a/pype/aport/original/pipeline.py +++ b/pype/aport/original/pipeline.py @@ -55,7 +55,7 @@ def publish(json_data_path, staging_dir=None): return_json_path = os.path.join(staging_dir, "return_data.json") log.debug("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") args = [pype_start, "--publish", diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index ad92c299bb..7142548fa6 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -26,7 +26,7 @@ class ClockifySettings(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_ROOT') + pype_setup = os.getenv('PYPE_ROOT') items = [pype_setup, "app", "resources", "icon.png"] fname = os.path.sep.join(items) icon = QtGui.QIcon(fname) diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 04ebd59ae4..5520087032 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -28,7 +28,7 @@ class Login_Dialog_ui(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_ROOT') + pype_setup = os.getenv('PYPE_ROOT') items = [pype_setup, "app", "resources", "icon.png"] fname = os.path.sep.join(items) icon = QtGui.QIcon(fname) From 0411c9e5d467dcf07d9f4992774f528dba585f41 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 26 Apr 2019 14:29:33 +0100 Subject: [PATCH 120/193] Clean up unused modules. --- pype/maya/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 0c00efb8ec..5636e4b994 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -2,7 +2,7 @@ import os import logging import weakref -from maya import utils, cmds, mel +from maya import utils, cmds from avalon import api as avalon, pipeline, maya from avalon.maya.pipeline import IS_HEADLESS @@ -10,7 +10,6 @@ from avalon.tools import workfiles from pyblish import api as pyblish from ..lib import ( - update_task_from_path, any_outdated ) from . import menu From a8115e969231cd54f05e9ec6bfbe2845fcac0210 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 15:37:07 +0200 Subject: [PATCH 121/193] fixing statics server so it's possible to use with python 3.6 --- .../services/statics_server/statics_server.py | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py index 8fb9990355..90048d2ee2 100644 --- a/pype/services/statics_server/statics_server.py +++ b/pype/services/statics_server/statics_server.py @@ -1,6 +1,8 @@ import os import socket import http.server +import urllib +import posixpath import socketserver from Qt import QtCore @@ -11,8 +13,118 @@ DIRECTORY = os.path.sep.join([os.environ['PYPE_MODULE_ROOT'], 'res']) class Handler(http.server.SimpleHTTPRequestHandler): + directory=DIRECTORY def __init__(self, *args, **kwargs): - super().__init__(*args, directory=DIRECTORY, **kwargs) + super().__init__(*args, **kwargs) + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + parts = urllib.parse.urlsplit(self.path) + if not parts.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(HTTPStatus.MOVED_PERMANENTLY) + new_parts = (parts[0], parts[1], parts[2] + '/', + parts[3], parts[4]) + new_url = urllib.parse.urlunsplit(new_parts) + self.send_header("Location", new_url) + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + f = open(path, 'rb') + except OSError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return None + + try: + fs = os.fstat(f.fileno()) + # Use browser cache if possible + if ("If-Modified-Since" in self.headers + and "If-None-Match" not in self.headers): + # compare If-Modified-Since and time of last file modification + try: + ims = email.utils.parsedate_to_datetime( + self.headers["If-Modified-Since"]) + except (TypeError, IndexError, OverflowError, ValueError): + # ignore ill-formed values + pass + else: + if ims.tzinfo is None: + # obsolete format with no timezone, cf. + # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 + ims = ims.replace(tzinfo=datetime.timezone.utc) + if ims.tzinfo is datetime.timezone.utc: + # compare to UTC datetime of last modification + last_modif = datetime.datetime.fromtimestamp( + fs.st_mtime, datetime.timezone.utc) + # remove microseconds, like in If-Modified-Since + last_modif = last_modif.replace(microsecond=0) + + if last_modif <= ims: + self.send_response(HTTPStatus.NOT_MODIFIED) + self.end_headers() + f.close() + return None + + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", + self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + # Don't forget explicit trailing slash when normalizing. Issue17324 + trailing_slash = path.rstrip().endswith('/') + try: + path = urllib.parse.unquote(path, errors='surrogatepass') + except UnicodeDecodeError: + path = urllib.parse.unquote(path) + path = posixpath.normpath(path) + words = path.split('/') + words = filter(None, words) + path = self.directory + for word in words: + if os.path.dirname(word) or word in (os.curdir, os.pardir): + # Ignore components that are not a simple file/directory name + continue + path = os.path.join(path, word) + if trailing_slash: + path += '/' + return path class StaticsServer(QtCore.QThread): From aa035641fe56af4422cadccd4a86c46971675511 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 26 Apr 2019 14:50:21 +0100 Subject: [PATCH 122/193] Make starting workfiles on launch configurable. --- pype/maya/__init__.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 5636e4b994..8bfc4c8ee5 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -8,6 +8,7 @@ from avalon import api as avalon, pipeline, maya from avalon.maya.pipeline import IS_HEADLESS from avalon.tools import workfiles from pyblish import api as pyblish +from pypeapp import config from ..lib import ( any_outdated @@ -107,15 +108,25 @@ def on_init(_): # Force load objExport plug-in (requested by artists) cmds.loadPlugin("objExport", quiet=True) - # Force load objExport plug-in (requested by artists) - cmds.loadPlugin("spore", quiet=True) - from .customize import ( override_component_mask_commands, override_toolbox_ui ) safe_deferred(override_component_mask_commands) - safe_deferred(launch_workfiles_app) + + launch_workfiles = True + try: + presets = config.get_presets() + launch_workfiles = presets['tools']['workfiles']['start_on_app_launch'] + except KeyError: + log.info( + "Workfiles app start on launch configuration was not found." + " Defaulting to False." + ) + launch_workfiles = False + + if launch_workfiles: + safe_deferred(launch_workfiles_app) if not IS_HEADLESS: safe_deferred(override_toolbox_ui) From f5e5e407d39e1b7cf06651541f516ccfe95224d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:04:49 +0200 Subject: [PATCH 123/193] fix os import in ftrack app loader --- pype/ftrack/actions/action_application_loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index bc126fc691..1b0f48f9be 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -1,3 +1,4 @@ +import os import toml import time from pype.ftrack import AppAction From 8cf349df975c2021c9f8c9c9f1a909ed94d8a92d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:19:52 +0200 Subject: [PATCH 124/193] fix from develop branch --- pype/ftrack/actions/action_application_loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index bc126fc691..1b0f48f9be 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -1,3 +1,4 @@ +import os import toml import time from pype.ftrack import AppAction From 7b6eaf433a38d2a8dfe0e1648419775ec5112b80 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 26 Apr 2019 16:49:47 +0100 Subject: [PATCH 125/193] Validate required modules for AssetCreator action. --- pype/plugins/launcher/actions/AssetCreator.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pype/plugins/launcher/actions/AssetCreator.py b/pype/plugins/launcher/actions/AssetCreator.py index 579edebcea..9787aae002 100644 --- a/pype/plugins/launcher/actions/AssetCreator.py +++ b/pype/plugins/launcher/actions/AssetCreator.py @@ -1,9 +1,4 @@ -import os -import sys -import acre - from avalon import api, lib -from pype.tools import assetcreator from pype.api import Logger @@ -19,9 +14,23 @@ class AssetCreator(api.Action): def is_compatible(self, session): """Return whether the action is compatible with the session""" - if "AVALON_PROJECT" in session: - return True - return False + compatible = True + + # Check required modules. + module_names = [ + "ftrack_api", "ftrack_api_old", "pype.tools.assetcreator" + ] + for name in module_names: + try: + __import__(name) + except ImportError: + compatible = False + + # Check session environment. + if "AVALON_PROJECT" not in session: + compatible = False + + return compatible def process(self, session, **kwargs): asset = '' From 6e1a4a998768e787cf7961c71eb44cb421f66251 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:49:55 +0200 Subject: [PATCH 126/193] pyblish is launched through avalon tools now --- pype/standalonepublish/publish.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 215281bfaf..f16fbdd0d8 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -7,6 +7,7 @@ import string from avalon import io from avalon import api as avalon +from avalon.tools import publish as av_publish import pype from pypeapp import execute @@ -70,14 +71,14 @@ def publish(data, gui=True): "-pp", os.pathsep.join(pyblish.api.registered_paths()) ] - if gui: - args += ["gui"] - os.environ["PYBLISH_HOSTS"] = "shell" os.environ["ASAPUBLISH_INPATH"] = json_data_path - returncode = execute([ - sys.executable, "-u", "-m", "pyblish" - ] + args, env=os.environ) + if gui: + av_publish.show() + else: + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) io.uninstall() From cdb24724c1830f2f916808cce9e9cb6a908b0fd6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:57:45 +0200 Subject: [PATCH 127/193] changed name od module --- pype/standalonepublish/__init__.py | 4 ++-- .../{asapublish_module.py => standalonepublish_module.py} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename pype/standalonepublish/{asapublish_module.py => standalonepublish_module.py} (85%) diff --git a/pype/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py index cc6f33d47e..c7be80f189 100644 --- a/pype/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -1,4 +1,4 @@ -from .asapublish_module import ASAPublishModule +from .standalonepublish_module import StandAlonePublishModule from .app import ( show, cli @@ -9,4 +9,4 @@ __all__ = [ ] def tray_init(tray_widget, main_widget): - return ASAPublishModule(main_widget, tray_widget) + return StandAlonePublishModule(main_widget, tray_widget) diff --git a/pype/standalonepublish/asapublish_module.py b/pype/standalonepublish/standalonepublish_module.py similarity index 85% rename from pype/standalonepublish/asapublish_module.py rename to pype/standalonepublish/standalonepublish_module.py index d695065601..8fce2810b7 100644 --- a/pype/standalonepublish/asapublish_module.py +++ b/pype/standalonepublish/standalonepublish_module.py @@ -2,14 +2,14 @@ from .app import show from .widgets import QtWidgets -class ASAPublishModule: +class StandAlonePublishModule: def __init__(self, main_parent=None, parent=None): self.main_parent = main_parent self.parent_widget = parent def tray_menu(self, parent_menu): self.run_action = QtWidgets.QAction( - "ASAPublish", parent_menu + "Publish", parent_menu ) self.run_action.triggered.connect(show) parent_menu.addAction(self.run_action) From c47517a1f060ff627401ec173a08d370b176f84f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:58:14 +0200 Subject: [PATCH 128/193] representations have key representations instead of components --- pype/standalonepublish/widgets/widget_drop_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 90434e75f4..dd9072448b 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -367,7 +367,7 @@ class DropDataFrame(QtWidgets.QFrame): self.parent_widget.working_stop() def collect_data(self): - data = {'components' : []} + data = {'representations' : []} for item in self.items: - data['components'].append(item.collect_data()) + data['representations'].append(item.collect_data()) return data From 8553b042d562c709cd087f55772dce7fae4d0b67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 18:09:21 +0200 Subject: [PATCH 129/193] hide shadow widget only if is visible --- pype/standalonepublish/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 1a875505d7..31a7763ace 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -153,7 +153,8 @@ class Window(QtWidgets.QDialog): QtWidgets.QApplication.processEvents() def working_stop(self): - self.shadow_widget.setVisible(False) + if self.shadow_widget.isVisible(): + self.shadow_widget.setVisible(False) def validation(self): if not self.initialized: From a40818816ad554c2ec8b394bb9da0309164b0fc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 14:50:00 +0200 Subject: [PATCH 130/193] idle manager register Qaction and set icon to failed when service fails --- pype/services/idle_manager/idle_manager.py | 31 +++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py index f7d7f2b34e..57b827a37e 100644 --- a/pype/services/idle_manager/idle_manager.py +++ b/pype/services/idle_manager/idle_manager.py @@ -17,9 +17,14 @@ class IdleManager(QtCore.QThread): super(IdleManager, self).__init__() self.log = Logger().get_logger(self.__class__.__name__) self.signal_reset_timer.connect(self._reset_time) - self._failed = False + self.qaction = None + self.failed_icon = None self._is_running = False + def set_qaction(self, qaction, failed_icon): + self.qaction = qaction + self.failed_icon = failed_icon + def tray_start(self): self.start() @@ -34,10 +39,6 @@ class IdleManager(QtCore.QThread): self.time_signals[emit_time] = [] self.time_signals[emit_time].append(signal) - @property - def failed(self): - return self._failed - @property def is_running(self): return self._is_running @@ -55,20 +56,26 @@ class IdleManager(QtCore.QThread): thread_mouse.start() thread_keyboard = KeyboardThread(self.signal_reset_timer) thread_keyboard.start() - while self._is_running: - self.idle_time += 1 - if self.idle_time in self.time_signals: - for signal in self.time_signals[self.idle_time]: - signal.emit() - time.sleep(1) + try: + while self.is_running: + self.idle_time += 1 + if self.idle_time in self.time_signals: + for signal in self.time_signals[self.idle_time]: + signal.emit() + time.sleep(1) + except Exception: + self.log.warning( + 'Idle Manager service has failed', exc_info=True + ) + if self.qaction and self.failed_icon: + self.qaction.setIcon(self.failed_icon) thread_mouse.signal_stop.emit() thread_mouse.terminate() thread_mouse.wait() thread_keyboard.signal_stop.emit() thread_keyboard.terminate() thread_keyboard.wait() - self._failed = True self._is_running = False self.log.info('IdleManager has stopped') From 18f11d8f972f49e02ec18b2c2f1c17c9c356cfdc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 14:50:32 +0200 Subject: [PATCH 131/193] timers manager cant handle at the moment about failing --- pype/services/timers_manager/timers_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/services/timers_manager/timers_manager.py b/pype/services/timers_manager/timers_manager.py index 319e4c6910..6f10a0ec68 100644 --- a/pype/services/timers_manager/timers_manager.py +++ b/pype/services/timers_manager/timers_manager.py @@ -25,7 +25,6 @@ class TimersManager(metaclass=Singleton): when user idles for a long time (set in presets). """ modules = [] - failed = False is_running = False last_task = None From 460bd8fff08ef982a73e4c19761f634e10bc4e0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 14:50:58 +0200 Subject: [PATCH 132/193] statics server handle about failing and can register QAction --- .../services/statics_server/statics_server.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py index 90048d2ee2..43fb379d37 100644 --- a/pype/services/statics_server/statics_server.py +++ b/pype/services/statics_server/statics_server.py @@ -132,11 +132,11 @@ class StaticsServer(QtCore.QThread): Idle time resets on keyboard/mouse input. Is able to emit signals at specific time idle. """ - def __init__(self): super(StaticsServer, self).__init__() + self.qaction = None + self.failed_icon = None self._is_running = False - self._failed = False self.log = Logger().get_logger(self.__class__.__name__) try: self.presets = config.get_presets().get( @@ -146,6 +146,10 @@ class StaticsServer(QtCore.QThread): self.port = self.find_port() + def set_qaction(self, qaction, failed_icon): + self.qaction = qaction + self.failed_icon = failed_icon + def tray_start(self): self.start() @@ -153,10 +157,6 @@ class StaticsServer(QtCore.QThread): def is_running(self): return self._is_running - @property - def failed(self): - return self._failed - def stop(self): self._is_running = False @@ -167,8 +167,12 @@ class StaticsServer(QtCore.QThread): while self._is_running: httpd.handle_request() except Exception: - self._failed = True - self._is_running = False + self.log.warning( + 'Statics Server service has failed', exc_info=True + ) + self._is_running = False + if self.qaction and self.failed_icon: + self.qaction.setIcon(self.failed_icon) def find_port(self): start_port = self.presets['default_port'] From 76c9537e159eb44c14520b0f895e20e7468ebe9a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 14:52:55 +0200 Subject: [PATCH 133/193] statics server is suitable for more python versions --- pype/services/statics_server/statics_server.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py index 43fb379d37..8655cd9df9 100644 --- a/pype/services/statics_server/statics_server.py +++ b/pype/services/statics_server/statics_server.py @@ -1,6 +1,9 @@ import os +import sys +import datetime import socket import http.server +from http import HTTPStatus import urllib import posixpath import socketserver @@ -13,9 +16,14 @@ DIRECTORY = os.path.sep.join([os.environ['PYPE_MODULE_ROOT'], 'res']) class Handler(http.server.SimpleHTTPRequestHandler): - directory=DIRECTORY def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + py_version = sys.version.split('.') + # If python version is 3.7 or higher + if int(py_version[0]) >= 3 and int(py_version[1]) >= 7: + super().__init__(*args, directory=DIRECTORY, **kwargs) + else: + self.directory = DIRECTORY + super().__init__(*args, **kwargs) def send_head(self): """Common code for GET and HEAD commands. @@ -62,7 +70,7 @@ class Handler(http.server.SimpleHTTPRequestHandler): and "If-None-Match" not in self.headers): # compare If-Modified-Since and time of last file modification try: - ims = email.utils.parsedate_to_datetime( + ims = http.server.email.utils.parsedate_to_datetime( self.headers["If-Modified-Since"]) except (TypeError, IndexError, OverflowError, ValueError): # ignore ill-formed values From 362c1248f85f87d379c9985273e624e6f6127030 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 15:10:08 +0200 Subject: [PATCH 134/193] temporarily fixed crashing on merge/split --- pype/standalonepublish/widgets/widget_drop_frame.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index dd9072448b..644f53a732 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,6 +11,7 @@ class DropDataFrame(QtWidgets.QFrame): super().__init__() self.parent_widget = parent self.items = [] + self.removed = [] self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) @@ -117,6 +118,7 @@ class DropDataFrame(QtWidgets.QFrame): index = self.components_list.widget_index(item) self.components_list.remove_widget(index) if item in self.items: + self.removed.append(item) self.items.remove(item) self._refresh_view() From 7c7b9256716b5a7e70c3dc8e4b6c7e8cbdb9d701 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sun, 28 Apr 2019 20:47:32 +0200 Subject: [PATCH 135/193] import ftrack_api is specified now from pype.vendor --- pype/ftrack/credentials.py | 2 +- pype/ftrack/events/action_sync_to_avalon.py | 2 +- pype/ftrack/events/event_del_avalon_id_from_new.py | 2 +- pype/ftrack/events/event_next_task_update.py | 2 +- pype/ftrack/events/event_radio_buttons.py | 2 +- pype/ftrack/events/event_sync_to_avalon.py | 2 +- pype/ftrack/events/event_test.py | 2 +- pype/ftrack/events/event_thumbnail_updates.py | 2 +- pype/ftrack/events/event_version_to_task_statuses.py | 2 +- pype/ftrack/lib/ftrack_base_handler.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/ftrack/credentials.py b/pype/ftrack/credentials.py index 30d503c534..836af73c61 100644 --- a/pype/ftrack/credentials.py +++ b/pype/ftrack/credentials.py @@ -1,6 +1,6 @@ import os import json -import ftrack_api +from pype.vendor import ftrack_api import appdirs diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index dd9534a764..8a5be1c100 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -2,8 +2,8 @@ import os import sys import argparse import logging -import ftrack_api import json +from pype.vendor import ftrack_api from pype.ftrack import BaseAction, lib diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py index 7659191637..f27a329429 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent, get_ca_mongoid from pype.ftrack.events.event_sync_to_avalon import Sync_to_Avalon diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index e677e53fb2..1ae06050bc 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent import operator diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py index f96d90307d..769115f045 100644 --- a/pype/ftrack/events/event_radio_buttons.py +++ b/pype/ftrack/events/event_radio_buttons.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 1deaa3d17e..9dd7355d5e 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent, lib diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py index e4da4cd44e..f6746f2535 100644 --- a/pype/ftrack/events/event_test.py +++ b/pype/ftrack/events/event_test.py @@ -1,7 +1,7 @@ import os import sys import re -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index 50089e26b8..042f6cc600 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index d1393e622e..8b14e025d3 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 24ece4f11d..cfe89a4d40 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -1,7 +1,7 @@ import functools import time from pype import api as pype -import ftrack_api +from pype.vendor import ftrack_api class MissingPermision(Exception): From 447c63fc331d1eb7b740a5037a972a0ed8d4bca4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 11:33:46 +0200 Subject: [PATCH 136/193] standalone publisher now handles duplicated representation names --- pype/standalonepublish/app.py | 16 +---- .../widgets/widget_component_item.py | 13 ++++ .../widgets/widget_components.py | 26 ++++++-- .../widgets/widget_drop_frame.py | 60 +++++++++++++++++-- 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 31a7763ace..ba7dcaa978 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -92,10 +92,9 @@ class Window(QtWidgets.QDialog): return self._db def on_start(self): - self.initialized = True # Refresh asset input in Family widget self.on_asset_changed() - self.validation() + self.widget_components.validation() self.shadow_widget = ShadowWidget(self) self.shadow_widget.setVisible(False) @@ -156,19 +155,10 @@ class Window(QtWidgets.QDialog): if self.shadow_widget.isVisible(): self.shadow_widget.setVisible(False) - def validation(self): - if not self.initialized: - return - valid = self.valid_family and self.valid_components - self.widget_components.set_valid(valid) - def set_valid_family(self, valid): self.valid_family = valid - self.validation() - - def set_valid_components(self, valid): - self.valid_components = valid - self.validation() + if hasattr(self, 'widget_components'): + self.widget_components.validation() def collect_data(self): data = {} diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 6dec892d91..2e0df9a00c 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -13,9 +13,11 @@ class ComponentItem(QtWidgets.QFrame): signal_remove = QtCore.Signal(object) signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) + signal_repre_change = QtCore.Signal(object, object) def __init__(self, parent, main_parent): super().__init__() + self.has_valid_repre = True self.actions = [] self.resize(290, 70) self.setMinimumSize(QtCore.QSize(0, 70)) @@ -183,6 +185,7 @@ class ComponentItem(QtWidgets.QFrame): self.remove.clicked.connect(self._remove) self.thumbnail.clicked.connect(self._thumbnail_clicked) self.preview.clicked.connect(self._preview_clicked) + self.input_repre.textChanged.connect(self._handle_duplicate_repre) name = data['name'] representation = data['representation'] ext = data['ext'] @@ -238,6 +241,13 @@ class ComponentItem(QtWidgets.QFrame): self.btn_action_menu.clicked.connect(self.show_actions) self.action_menu.setStyleSheet(style.load_stylesheet()) + def set_repre_name_valid(self, valid): + self.has_valid_repre = valid + if valid: + self.input_repre.setStyleSheet("") + else: + self.input_repre.setStyleSheet("border: 1px solid red;") + def split_sequence(self): self.parent_widget.split_items(self) @@ -257,6 +267,9 @@ class ComponentItem(QtWidgets.QFrame): def _preview_clicked(self): self.signal_preview.emit(self) + def _handle_duplicate_repre(self, repre_name): + self.signal_repre_change.emit(self, repre_name) + def is_thumbnail(self): return self.thumbnail.checked diff --git a/pype/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py index 5cc66de0b5..326aefe693 100644 --- a/pype/standalonepublish/widgets/widget_components.py +++ b/pype/standalonepublish/widgets/widget_components.py @@ -7,6 +7,11 @@ from .. import publish class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): super().__init__() + self.initialized = False + self.valid_components = False + self.valid_family = False + self.valid_repre_names = False + body = QtWidgets.QWidget() self.parent_widget = parent self.drop_frame = DropDataFrame(self) @@ -39,12 +44,25 @@ class ComponentsWidget(QtWidgets.QWidget): self.btn_browse.clicked.connect(self._browse) self.btn_publish.clicked.connect(self._publish) + self.initialized = True - def set_valid(self, in_bool): - self.btn_publish.setEnabled(in_bool) + def validation(self): + if self.initialized is False: + return + valid = ( + self.parent_widget.valid_family and + self.valid_components and + self.valid_repre_names + ) + self.btn_publish.setEnabled(valid) - def set_valid_components(self, in_bool): - self.parent_widget.set_valid_components(in_bool) + def set_valid_components(self, valid): + self.valid_components = valid + self.validation() + + def set_valid_repre_names(self, valid): + self.valid_repre_names = valid + self.validation() def process_mime_data(self, mime_data): self.drop_frame.process_ent_mime(mime_data) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 644f53a732..89c4352717 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -1,4 +1,5 @@ import os +import re import clique import subprocess from pypeapp import config @@ -83,6 +84,7 @@ class DropDataFrame(QtWidgets.QFrame): new_component.signal_thumbnail.connect( self._set_thumbnail ) + new_component.signal_repre_change.connect(self.repre_name_changed) for action in actions: new_component.add_action(action) @@ -114,13 +116,19 @@ class DropDataFrame(QtWidgets.QFrame): checked_item.change_preview(False) in_item.change_preview() - def _remove_item(self, item): - index = self.components_list.widget_index(item) + def _remove_item(self, in_item): + index = self.components_list.widget_index(in_item) self.components_list.remove_widget(index) - if item in self.items: - self.removed.append(item) - self.items.remove(item) + if in_item in self.items: + self.removed.append(in_item) + self.items.remove(in_item) self._refresh_view() + if in_item.has_valid_repre: + return + for item in self.items: + if item.has_valid_repre: + continue + self.repre_name_changed(item, item.input_repre.text()) def _refresh_view(self): _bool = len(self.items) == 0 @@ -338,8 +346,50 @@ class DropDataFrame(QtWidgets.QFrame): actions.append('split') if found is False: + new_repre = self.handle_new_repre_name(data['representation']) + data['representation'] = new_repre self._add_item(data, actions) + def handle_new_repre_name(self, repre_name): + renamed = False + for item in self.items: + if repre_name == item.input_repre.text(): + check_regex = '\(\w+\)$' + result = re.findall(check_regex, repre_name) + next_num = 2 + if len(result) == 1: + repre_name = repre_name.replace(result[0], '') + next_num = int(result[0].replace('(', '').replace(')', '')) + next_num += 1 + repre_name = '{}({})'.format(repre_name, next_num) + renamed = True + break + if renamed: + return self.handle_new_repre_name(repre_name) + return repre_name + + def repre_name_changed(self, in_item, repre_name): + is_valid = True + for item in self.items: + if item == in_item: + continue + if item.input_repre.text() == repre_name: + item.set_repre_name_valid(False) + in_item.set_repre_name_valid(False) + is_valid = False + global_valid = is_valid + if is_valid: + in_item.set_repre_name_valid(True) + for item in self.items: + if item.has_valid_repre: + continue + self.repre_name_changed(item, item.input_repre.text()) + for item in self.items: + if not item.has_valid_repre: + global_valid = False + break + self.parent_widget.set_valid_repre_names(global_valid) + def merge_items(self, in_item): self.parent_widget.working_start() items = [] From 060b0e3008f5baeed3c385083ed3ccff7561a24e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 11:37:08 +0200 Subject: [PATCH 137/193] all video and image files can be set as thumbnail now --- pype/standalonepublish/widgets/widget_drop_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 89c4352717..5aa9d7ea61 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -277,8 +277,8 @@ class DropDataFrame(QtWidgets.QFrame): icon += 's' data['icon'] = icon data['thumb'] = ( - ext in self.presets['extensions']['thumbnailable'] and - data['is_sequence'] is False + ext in self.presets['extensions']['image_file'] or + ext in self.presets['extensions']['video_file'] ) data['prev'] = ext in self.presets['extensions']['video_file'] From aecbd28fe6dfe639ef04ee8bf18ddce298546358 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 12:13:49 +0200 Subject: [PATCH 138/193] standalone publisher does not allow empty representation name --- .../widgets/widget_drop_frame.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 5aa9d7ea61..fa8ec66b49 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -370,13 +370,17 @@ class DropDataFrame(QtWidgets.QFrame): def repre_name_changed(self, in_item, repre_name): is_valid = True - for item in self.items: - if item == in_item: - continue - if item.input_repre.text() == repre_name: - item.set_repre_name_valid(False) - in_item.set_repre_name_valid(False) - is_valid = False + if repre_name.strip() == '': + in_item.set_repre_name_valid(False) + is_valid = False + else: + for item in self.items: + if item == in_item: + continue + if item.input_repre.text() == repre_name: + item.set_repre_name_valid(False) + in_item.set_repre_name_valid(False) + is_valid = False global_valid = is_valid if is_valid: in_item.set_repre_name_valid(True) From 36f5c91d6a80feb77823ac15183a6cb064f1b4ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 12:15:00 +0200 Subject: [PATCH 139/193] valid repre name is set to True on first representations --- pype/standalonepublish/widgets/widget_drop_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index fa8ec66b49..cd5d8fc29e 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -89,7 +89,8 @@ class DropDataFrame(QtWidgets.QFrame): new_component.add_action(action) self.items.append(new_component) - + if len(self.items) == 1: + self.parent_widget.set_valid_repre_names(True) self._refresh_view() def _set_thumbnail(self, in_item): From acf1b44b8a98f66b92046f3ea7356a716219f411 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:51:22 +0200 Subject: [PATCH 140/193] basic docstrings --- pype/standalonepublish/app.py | 46 ++++++++++++++++++++++++++++--- pype/standalonepublish/publish.py | 14 ++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index ba7dcaa978..8956155230 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -14,6 +14,11 @@ module = sys.modules[__name__] module.window = None class Window(QtWidgets.QDialog): + """Main window of Standalone publisher. + + :param parent: Main widget that cares about all GUIs + :type parent: QtWidgets.QMainWindow + """ _db = DbConnector() _jobs = {} valid_family = False @@ -24,7 +29,7 @@ class Window(QtWidgets.QDialog): NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): - super(Window, self).__init__() + super(Window, self).__init__(parent=parent) self._db.install() self.setWindowTitle("Standalone Publish") @@ -89,16 +94,23 @@ class Window(QtWidgets.QDialog): @property def db(self): + ''' Returns DB object for MongoDB I/O + ''' return self._db def on_start(self): + ''' Things must be done when initilized. + ''' # Refresh asset input in Family widget self.on_asset_changed() self.widget_components.validation() + # Initializing shadow widget self.shadow_widget = ShadowWidget(self) self.shadow_widget.setVisible(False) def resizeEvent(self, event=None): + ''' Helps resize shadow widget + ''' position_x = (self.frameGeometry().width()-self.shadow_widget.frameGeometry().width())/2 position_y = (self.frameGeometry().height()-self.shadow_widget.frameGeometry().height())/2 self.shadow_widget.move(position_x, position_y) @@ -109,6 +121,8 @@ class Window(QtWidgets.QDialog): super().resizeEvent(event) def get_avalon_parent(self, entity): + ''' Avalon DB entities helper - get all parents (exclude project). + ''' parent_id = entity['data']['visualParent'] parents = [] if parent_id is not None: @@ -118,15 +132,19 @@ class Window(QtWidgets.QDialog): return parents def echo(self, message): + ''' Shows message in label that disappear in 5s + :param message: Message that will be displayed + :type message: str + ''' self.label_message.setText(str(message)) QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) def on_asset_changed(self): - """Callback on asset selection changed + '''Callback on asset selection changed - This updates the task view. + Updates the task view. - """ + ''' selected = self.widget_assets.get_selected_assets() if len(selected) == 1: self.valid_parent = True @@ -138,12 +156,22 @@ class Window(QtWidgets.QDialog): self.widget_family.on_data_changed() def keyPressEvent(self, event): + ''' Handling Ctrl+V KeyPress event + Can handle: + - files/folders in clipboard (tested only on Windows OS) + - copied path of file/folder in clipboard ('c:/path/to/folder') + ''' if event.key() == QtCore.Qt.Key_V and event.modifiers() == QtCore.Qt.ControlModifier: clip = QtWidgets.QApplication.clipboard() self.widget_components.process_mime_data(clip) super().keyPressEvent(event) def working_start(self, msg=None): + ''' Shows shadowed foreground with message + :param msg: Message that will be displayed + (set to `Please wait...` if `None` entered) + :type msg: str + ''' if msg is None: msg = 'Please wait...' self.shadow_widget.message = msg @@ -152,15 +180,25 @@ class Window(QtWidgets.QDialog): QtWidgets.QApplication.processEvents() def working_stop(self): + ''' Hides shadowed foreground + ''' if self.shadow_widget.isVisible(): self.shadow_widget.setVisible(False) def set_valid_family(self, valid): + ''' Sets `valid_family` attribute for validation + + .. note:: + if set to `False` publishing is not possible + ''' self.valid_family = valid + # If widget_components not initialized yet if hasattr(self, 'widget_components'): self.widget_components.validation() def collect_data(self): + ''' Collecting necessary data for pyblish from child widgets + ''' data = {} data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index f16fbdd0d8..0e811aae52 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -15,7 +15,9 @@ from pypeapp import execute import pyblish.api +# Registers Global pyblish plugins pype.install() +# Registers Standalone pyblish plugins PUBLISH_PATH = os.path.sep.join( [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] ) @@ -23,6 +25,12 @@ pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): + ''' Sets context for pyblish (must be done before pyblish is launched) + :param project: Name of `Project` where instance should be published + :type project: str + :param asset: Name of `Asset` where instance should be published + :type asset: str + ''' os.environ["AVALON_PROJECT"] = project io.Session["AVALON_PROJECT"] = project os.environ["AVALON_ASSET"] = asset @@ -56,6 +64,12 @@ def set_context(project, asset, app): def publish(data, gui=True): + ''' Launches Pyblish (GUI by default) + :param data: Should include data for pyblish and standalone collector + :type data: dict + :param gui: Pyblish will be launched in GUI mode if set to True + :type gui: bool + ''' io.install() # Create hash name folder in temp From f330a062c789ce7ad0636e8b3d4b7bdf6047c5aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:56:34 +0200 Subject: [PATCH 141/193] fixed bug: crashing when publisher is closed before echo message disappered --- pype/standalonepublish/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 8956155230..956cdb6300 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -137,7 +137,15 @@ class Window(QtWidgets.QDialog): :type message: str ''' self.label_message.setText(str(message)) - QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) + def clear_text(): + ''' Helps prevent crash if this Window object + is deleted before 5s passed + ''' + try: + self.label_message.set_text("") + except: + pass + QtCore.QTimer.singleShot(5000, lambda: clear_text()) def on_asset_changed(self): '''Callback on asset selection changed From 4d4226b1290f542f1ba17ff60fd021f138083c79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:57:13 +0200 Subject: [PATCH 142/193] created show method in module class so parent is set --- pype/standalonepublish/standalonepublish_module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/standalonepublish_module.py b/pype/standalonepublish/standalonepublish_module.py index 8fce2810b7..703f457138 100644 --- a/pype/standalonepublish/standalonepublish_module.py +++ b/pype/standalonepublish/standalonepublish_module.py @@ -11,5 +11,8 @@ class StandAlonePublishModule: self.run_action = QtWidgets.QAction( "Publish", parent_menu ) - self.run_action.triggered.connect(show) + self.run_action.triggered.connect(self.show) parent_menu.addAction(self.run_action) + + def show(self): + show(self.main_parent, False) From b5e0069049fecff284a433859e9ab31b80403bdb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:58:34 +0200 Subject: [PATCH 143/193] renamed ASAPUBLISH_INPATH environment to SAPUBLISH_INPATH --- pype/plugins/standalonepublish/publish/collect_context.py | 2 +- pype/standalonepublish/publish.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index d063bcf2dd..26411dd132 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -29,7 +29,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): def process(self, context): # get json paths from os and load them io.install() - json_path = os.environ.get("ASAPUBLISH_INPATH") + json_path = os.environ.get("SAPUBLISH_INPATH") with open(json_path, "r") as f: in_data = json.load(f) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 0e811aae52..db41f68eb4 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -86,7 +86,7 @@ def publish(data, gui=True): ] os.environ["PYBLISH_HOSTS"] = "shell" - os.environ["ASAPUBLISH_INPATH"] = json_data_path + os.environ["SAPUBLISH_INPATH"] = json_data_path if gui: av_publish.show() From 27fb79f9f30ea9bd66e54070b5f566efa67517d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 17:22:41 +0200 Subject: [PATCH 144/193] fixed representations key in collector --- pype/plugins/standalonepublish/publish/collect_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 26411dd132..38ceb4dbd1 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -51,7 +51,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instances = [] - for component in in_data['components']: + for component in in_data['representations']: instance = context.create_instance(subset) # instance.add(node) From a72c517bd275876e22da8272a6362914303bbb6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 17:22:55 +0200 Subject: [PATCH 145/193] added output json path to collector --- .../standalonepublish/publish/collect_context.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 38ceb4dbd1..52b44f61e1 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -29,11 +29,15 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): def process(self, context): # get json paths from os and load them io.install() - json_path = os.environ.get("SAPUBLISH_INPATH") - with open(json_path, "r") as f: + input_json_path = os.environ.get("SAPUBLISH_INPATH") + output_json_path = os.environ.get("SAPUBLISH_OUTPATH") + + context.data["stagingDir"] = os.path.dirname(input_json_path) + context.data["returnJsonPath"] = output_json_path + + with open(input_json_path, "r") as f: in_data = json.load(f) - context.data["stagingDir"] = os.path.dirname(json_path) project_name = in_data['project'] asset_name = in_data['asset'] family = in_data['family'] From ed2fb8b7cddb782a416f92058c6855832b18fde0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:32:58 +0200 Subject: [PATCH 146/193] fixed removing representations from list --- .../widgets/widget_components_list.py | 1 - .../widgets/widget_drop_frame.py | 39 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_components_list.py b/pype/standalonepublish/widgets/widget_components_list.py index 357bd1e671..f85e9f0aa6 100644 --- a/pype/standalonepublish/widgets/widget_components_list.py +++ b/pype/standalonepublish/widgets/widget_components_list.py @@ -46,7 +46,6 @@ class ComponentsList(QtWidgets.QTableWidget): return row def remove_widget(self, row): - self.removeCellWidget(row, self._main_column) self.removeRow(row) def move_widget(self, widget, newRow): diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index cd5d8fc29e..0048989707 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,8 +11,6 @@ class DropDataFrame(QtWidgets.QFrame): def __init__(self, parent): super().__init__() self.parent_widget = parent - self.items = [] - self.removed = [] self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) @@ -88,14 +86,13 @@ class DropDataFrame(QtWidgets.QFrame): for action in actions: new_component.add_action(action) - self.items.append(new_component) - if len(self.items) == 1: + if len(self.components_list.widgets()) == 1: self.parent_widget.set_valid_repre_names(True) self._refresh_view() def _set_thumbnail(self, in_item): checked_item = None - for item in self.items: + for item in self.components_list.widgets(): if item.is_thumbnail(): checked_item = item break @@ -107,7 +104,7 @@ class DropDataFrame(QtWidgets.QFrame): def _set_preview(self, in_item): checked_item = None - for item in self.items: + for item in self.components_list.widgets(): if item.is_preview(): checked_item = item break @@ -118,21 +115,21 @@ class DropDataFrame(QtWidgets.QFrame): in_item.change_preview() def _remove_item(self, in_item): - index = self.components_list.widget_index(in_item) - self.components_list.remove_widget(index) - if in_item in self.items: - self.removed.append(in_item) - self.items.remove(in_item) + valid_repre = in_item.has_valid_repre is True + + self.components_list.remove_widget( + self.components_list.widget_index(in_item) + ) self._refresh_view() - if in_item.has_valid_repre: + if valid_repre: return - for item in self.items: + for item in self.components_list.widgets(): if item.has_valid_repre: continue self.repre_name_changed(item, item.input_repre.text()) def _refresh_view(self): - _bool = len(self.items) == 0 + _bool = len(self.components_list.widgets()) == 0 self.components_list.setVisible(not _bool) self.drop_widget.setVisible(_bool) @@ -287,7 +284,7 @@ class DropDataFrame(QtWidgets.QFrame): new_is_seq = data['is_sequence'] found = False - for item in self.items: + for item in self.components_list.widgets(): if data['ext'] != item.in_data['ext']: continue if data['folder_path'] != item.in_data['folder_path']: @@ -353,7 +350,7 @@ class DropDataFrame(QtWidgets.QFrame): def handle_new_repre_name(self, repre_name): renamed = False - for item in self.items: + for item in self.components_list.widgets(): if repre_name == item.input_repre.text(): check_regex = '\(\w+\)$' result = re.findall(check_regex, repre_name) @@ -375,7 +372,7 @@ class DropDataFrame(QtWidgets.QFrame): in_item.set_repre_name_valid(False) is_valid = False else: - for item in self.items: + for item in self.components_list.widgets(): if item == in_item: continue if item.input_repre.text() == repre_name: @@ -385,11 +382,11 @@ class DropDataFrame(QtWidgets.QFrame): global_valid = is_valid if is_valid: in_item.set_repre_name_valid(True) - for item in self.items: + for item in self.components_list.widgets(): if item.has_valid_repre: continue self.repre_name_changed(item, item.input_repre.text()) - for item in self.items: + for item in self.components_list.widgets(): if not item.has_valid_repre: global_valid = False break @@ -400,7 +397,7 @@ class DropDataFrame(QtWidgets.QFrame): items = [] in_paths = in_item.in_data['files'] paths = in_paths - for item in self.items: + for item in self.components_list.widgets(): if item.in_data['files'] == in_paths: items.append(item) continue @@ -425,6 +422,6 @@ class DropDataFrame(QtWidgets.QFrame): def collect_data(self): data = {'representations' : []} - for item in self.items: + for item in self.components_list.widgets(): data['representations'].append(item.collect_data()) return data From 2005484fe18f937596c1a3d58b319be9491bad59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:34:55 +0200 Subject: [PATCH 147/193] widget components is ready for handling publish result --- pype/standalonepublish/widgets/widget_components.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py index 326aefe693..1e1fdf88e3 100644 --- a/pype/standalonepublish/widgets/widget_components.py +++ b/pype/standalonepublish/widgets/widget_components.py @@ -119,6 +119,10 @@ class ComponentsWidget(QtWidgets.QWidget): publish.set_context( data['project'], data['asset'], 'standalonepublish' ) - publish.publish(data) + result = publish.publish(data) + # Clear widgets from components list if publishing was successful + if result: + self.drop_frame.components_list.clear_widgets() + self.drop_frame._refresh_view() finally: self.working_stop() From 372ed195f562fe45bfb39bd2176fc4fe82a342b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:36:03 +0200 Subject: [PATCH 148/193] cli pyblish moved back, avalon_api_pyblish launch kept for sure --- pype/standalonepublish/publish.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index db41f68eb4..05c889a2d6 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -64,6 +64,13 @@ def set_context(project, asset, app): def publish(data, gui=True): + # cli pyblish seems like better solution + return cli_publish(data, gui) + # # this uses avalon pyblish launch tool + # avalon_api_publish(data, gui) + + +def avalon_api_publish(data, gui=True): ''' Launches Pyblish (GUI by default) :param data: Should include data for pyblish and standalone collector :type data: dict @@ -74,7 +81,7 @@ def publish(data, gui=True): # Create hash name folder in temp chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) - staging_dir = tempfile.mkdtemp(chars)#.replace("\\", "/") + staging_dir = tempfile.mkdtemp(chars) # create also json and fill with data json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' @@ -96,3 +103,46 @@ def publish(data, gui=True): ] + args, env=os.environ) io.uninstall() + + +def cli_publish(data, gui=True): + io.install() + + # Create hash name folder in temp + chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) + staging_dir = tempfile.mkdtemp(chars) + + # create json for return data + return_data_path = ( + staging_dir + os.path.basename(staging_dir) + 'return.json' + ) + # create also json and fill with data + json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' + with open(json_data_path, 'w') as outfile: + json.dump(data, outfile) + + args = [ + "-pp", os.pathsep.join(pyblish.api.registered_paths()) + ] + + if gui: + args += ["gui"] + + os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["SAPUBLISH_INPATH"] = json_data_path + os.environ["SAPUBLISH_OUTPATH"] = return_data_path + + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) + + result = {} + if os.path.exists(json_data_path): + with open(json_data_path, "r") as f: + result = json.load(f) + + io.uninstall() + # TODO: check if was pyblish successful + # if successful return True + print('Check result here') + return False From 1ed32627c55272267f2f6c1d16f6f96bec51d95b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 May 2019 10:14:44 +0200 Subject: [PATCH 149/193] bug fixed ftrack_api symbol not used from imported module but directly imported --- pype/ftrack/lib/ftrack_base_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index cfe89a4d40..29395fc874 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -2,6 +2,7 @@ import functools import time from pype import api as pype from pype.vendor import ftrack_api +from pype.vendor.ftrack_api.symbol import NOT_SET as ftrack_api_NOT_SET class MissingPermision(Exception): @@ -196,7 +197,7 @@ class BaseHandler(object): _entities = event['data'].get('entities_object', None) if ( _entities is None or - _entities[0].get('link', None) == ftrack_api.symbol.NOT_SET + _entities[0].get('link', None) == ftrack_api_NOT_SET ): _entities = self._get_entities(event) From 91ab9e3c5f0a0472c63fecc5df691200a2c93db2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 May 2019 12:09:25 +0200 Subject: [PATCH 150/193] fixed ftrack_api import in ftrack_base_handler --- pype/ftrack/lib/ftrack_base_handler.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 29395fc874..63d4ff0ce9 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -2,7 +2,7 @@ import functools import time from pype import api as pype from pype.vendor import ftrack_api -from pype.vendor.ftrack_api.symbol import NOT_SET as ftrack_api_NOT_SET +from pype.vendor.ftrack_api import session as fa_session class MissingPermision(Exception): @@ -197,7 +197,9 @@ class BaseHandler(object): _entities = event['data'].get('entities_object', None) if ( _entities is None or - _entities[0].get('link', None) == ftrack_api_NOT_SET + _entities[0].get( + 'link', None + ) == fa_session.ftrack_api.symbol.NOT_SET ): _entities = self._get_entities(event) @@ -302,7 +304,7 @@ class BaseHandler(object): # Launch preactions for preaction in self.preactions: - event = ftrack_api.event.base.Event( + event = fa_session.ftrack_api.event.base.Event( topic='ftrack.action.launch', data=dict( actionIdentifier=preaction, @@ -314,7 +316,7 @@ class BaseHandler(object): ) session.event_hub.publish(event, on_error='ignore') # Relaunch this action - event = ftrack_api.event.base.Event( + event = fa_session.ftrack_api.event.base.Event( topic='ftrack.action.launch', data=dict( actionIdentifier=self.identifier, @@ -415,7 +417,7 @@ class BaseHandler(object): 'applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) self.session.event_hub.publish( - ftrack_api.event.base.Event( + fa_session.ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict( type='message', @@ -438,7 +440,7 @@ class BaseHandler(object): ).format(user_id) self.session.event_hub.publish( - ftrack_api.event.base.Event( + fa_session.ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict( type='widget', From 1cc6081f754d134535d5e77a393f8b0c3e63c628 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 9 May 2019 17:45:52 +0200 Subject: [PATCH 151/193] add plugin for publishing --- .../publish/collect_context.py | 36 +- .../publish/collect_templates.py | 17 + .../standalonepublish/publish/collect_time.py | 12 + .../standalonepublish/publish/integrate.py | 429 ++++++++++++++++++ pype/standalonepublish/publish.py | 8 +- 5 files changed, 483 insertions(+), 19 deletions(-) create mode 100644 pype/plugins/standalonepublish/publish/collect_templates.py create mode 100644 pype/plugins/standalonepublish/publish/collect_time.py create mode 100644 pype/plugins/standalonepublish/publish/integrate.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 52b44f61e1..26ebc642fb 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -50,28 +50,28 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): }) context.data['project'] = project context.data['asset'] = asset - context.data['family'] = family - context.data['subset'] = subset - instances = [] + instance = context.create_instance(subset) + + instance.data.update({ + "subset": family + subset, + "asset": asset_name, + "label": family + subset, + "name": family + subset, + "family": family, + "families": [family, 'ftrack'], + }) + self.log.info("collected instance: {}".format(instance.data)) + + instance.data["files"] = list() + instance.data['destination_list'] = list() for component in in_data['representations']: - instance = context.create_instance(subset) # instance.add(node) - instance.data.update({ - "subset": subset, - "asset": asset_name, - "label": component['label'], - "name": component['representation'], - "subset": subset, - "family": family, - "is_thumbnail": component['thumbnail'], - "is_preview": component['preview'] - }) + instance.data["files"].append(component['files']) + instance.data['destination_list'].append(component['files']) + # "is_thumbnail": component['thumbnail'], + # "is_preview": component['preview'] - self.log.info("collected instance: {}".format(instance.data)) - instances.append(instance) - - context.data["instances"] = instances self.log.info(in_data) diff --git a/pype/plugins/standalonepublish/publish/collect_templates.py b/pype/plugins/standalonepublish/publish/collect_templates.py new file mode 100644 index 0000000000..b59b20892b --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_templates.py @@ -0,0 +1,17 @@ + +import pype.api as pype +from pypeapp import Anatomy + +import pyblish.api + + +class CollectTemplates(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder + label = "Collect Templates" + + def process(self, context): + # pype.load_data_from_templates() + context.data['anatomy'] = Anatomy() + self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/standalonepublish/publish/collect_time.py b/pype/plugins/standalonepublish/publish/collect_time.py new file mode 100644 index 0000000000..e0adc7dfc3 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_time.py @@ -0,0 +1,12 @@ +import pyblish.api +from avalon import api + + +class CollectTime(pyblish.api.ContextPlugin): + """Store global time at the time of publish""" + + label = "Collect Current Time" + order = pyblish.api.CollectorOrder + + def process(self, context): + context.data["time"] = api.time() diff --git a/pype/plugins/standalonepublish/publish/integrate.py b/pype/plugins/standalonepublish/publish/integrate.py new file mode 100644 index 0000000000..7157a6fb1a --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate.py @@ -0,0 +1,429 @@ +import os +import logging +import shutil + +import errno +import pyblish.api +from avalon import api, io +from avalon.vendor import filelink + + +log = logging.getLogger(__name__) + + +class IntegrateAsset(pyblish.api.InstancePlugin): + """Resolve any dependency issies + + This plug-in resolves any paths which, if not updated might break + the published file. + + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + """ + + label = "Integrate Asset" + order = pyblish.api.IntegratorOrder + families = ["animation", + "camera", + "look", + "mayaAscii", + "model", + "pointcache", + "vdbcache", + "setdress", + "assembly", + "layout", + "rig", + "vrayproxy", + "yetiRig", + "yeticache", + "nukescript", + "review", + "workfile", + "scene", + "ass"] + exclude_families = ["clip"] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + + self.register(instance) + + self.log.info("Integrating Asset in to the database ...") + self.integrate(instance) + + def register(self, instance): + # Required environment variables + PROJECT = api.Session["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"] + LOCATION = api.Session["AVALON_LOCATION"] + + context = instance.context + # Atomicity + # + # Guarantee atomic publishes - each asset contains + # an identical set of members. + # __ + # / o + # / \ + # | o | + # \ / + # o __/ + # + assert all(result["success"] for result in context.data["results"]), ( + "Atomicity not held, aborting.") + + # Assemble + # + # | + # v + # ---> <---- + # ^ + # | + # + # stagingdir = instance.data.get("stagingDir") + # assert stagingdir, ("Incomplete instance \"%s\": " + # "Missing reference to staging area." % instance) + + # extra check if stagingDir actually exists and is available + + # self.log.debug("Establishing staging directory @ %s" % stagingdir) + + # Ensure at least one file is set up for transfer in staging dir. + files = instance.data.get("files", []) + assert files, "Instance has no files to transfer" + assert isinstance(files, (list, tuple)), ( + "Instance 'files' must be a list, got: {0}".format(files) + ) + + project = io.find_one({"type": "project"}) + + asset = io.find_one({"type": "asset", + "name": ASSET, + "parent": project["_id"]}) + + assert all([project, asset]), ("Could not find current project or " + "asset '%s'" % ASSET) + + subset = self.get_subset(asset, instance) + + # get next version + latest_version = io.find_one({"type": "version", + "parent": subset["_id"]}, + {"name": True}, + sort=[("name", -1)]) + + next_version = 1 + if latest_version is not None: + next_version += latest_version["name"] + + self.log.info("Verifying version from assumed destination") + + # assumed_data = instance.data["assumedTemplateData"] + # assumed_version = assumed_data["version"] + # if assumed_version != next_version: + # raise AttributeError("Assumed version 'v{0:03d}' does not match" + # "next version in database " + # "('v{1:03d}')".format(assumed_version, + # next_version)) + + self.log.debug("Next version: v{0:03d}".format(next_version)) + + version_data = self.create_version_data(context, instance) + version = self.create_version(subset=subset, + version_number=next_version, + locations=[LOCATION], + data=version_data) + + self.log.debug("Creating version ...") + version_id = io.insert_one(version).inserted_id + + # Write to disk + # _ + # | | + # _| |_ + # ____\ / + # |\ \ / \ + # \ \ v \ + # \ \________. + # \|________| + # + root = api.registered_root() + hierarchy = "" + parents = io.find_one({ + "type": 'asset', + "name": ASSET + })['data']['parents'] + if parents and len(parents) > 0: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*parents) + + template_data = {"root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy} + + template_publish = project["config"]["template"]["publish"] + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + for files in instance.data["files"]: + + # Collection + # _______ + # |______|\ + # | |\| + # | || + # | || + # | || + # |_______| + # + + if isinstance(files, list): + collection = files + # Assert that each member has identical suffix + _, ext = os.path.splitext(collection[0]) + assert all(ext == os.path.splitext(name)[1] + for name in collection), ( + "Files had varying suffixes, this is a bug" + ) + + # assert not any(os.path.isabs(name) for name in collection) + + template_data["representation"] = ext[1:] + + for fname in collection: + + src = fname + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["publish"]["path"] + + instance.data["transfers"].append([src, dst]) + template = anatomy.templates["publish"]["path"] + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + _, ext = os.path.splitext(fname) + + template_data["representation"] = ext[1:] + + # src = os.path.join(stagingdir, fname) + src = fname + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["publish"]["path"] + + instance.data["transfers"].append([src, dst]) + template = anatomy.templates["publish"]["path"] + + representation = { + "schema": "pype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": ext[1:], + "data": {'path': dst, 'template': template}, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": { + "root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + # 'task': api.Session["AVALON_TASK"], + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": version["name"], + "hierarchy": hierarchy, + "representation": ext[1:] + } + } + + destination_list.append(dst) + instance.data['destination_list'] = destination_list + representations.append(representation) + + self.log.info("Registering {} items".format(len(representations))) + + io.insert_many(representations) + + def integrate(self, instance): + """Move the files + + Through `instance.data["transfers"]` + + Args: + instance: the instance to integrate + """ + + transfers = instance.data.get("transfers", list()) + + for src, dest in transfers: + self.log.info("Copying file .. {} -> {}".format(src, dest)) + self.copy_file(src, dest) + + # Produce hardlinked copies + # Note: hardlink can only be produced between two files on the same + # server/disk and editing one of the two will edit both files at once. + # As such it is recommended to only make hardlinks between static files + # to ensure publishes remain safe and non-edited. + hardlinks = instance.data.get("hardlinks", list()) + for src, dest in hardlinks: + self.log.info("Hardlinking file .. {} -> {}".format(src, dest)) + self.hardlink_file(src, dest) + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + shutil.copy(src, dst) + + def hardlink_file(self, src, dst): + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + filelink.create(src, dst, filelink.HARDLINK) + + def get_subset(self, asset, instance): + + subset = io.find_one({"type": "subset", + "parent": asset["_id"], + "name": instance.data["subset"]}) + + if subset is None: + subset_name = instance.data["subset"] + self.log.info("Subset '%s' not found, creating.." % subset_name) + + _id = io.insert_one({ + "schema": "avalon-core:subset-2.0", + "type": "subset", + "name": subset_name, + "data": {}, + "parent": asset["_id"] + }).inserted_id + + subset = io.find_one({"_id": _id}) + + return subset + + def create_version(self, subset, version_number, locations, data=None): + """ Copy given source to destination + + Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + locations (list): the currently registered locations + + Returns: + dict: collection of data to create a version + """ + # Imprint currently registered location + version_locations = [location for location in locations if + location is not None] + + return {"schema": "avalon-core:version-2.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "locations": version_locations, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context + instance: the current instance being published + + Returns: + dict: the required information with instance.data as key + """ + + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families + + self.log.debug("Registered root: {}".format(api.registered_root())) + # # create relative source path for DB + # try: + # source = instance.data['source'] + # except KeyError: + # source = context.data["currentFile"] + # + # relative_path = os.path.relpath(source, api.registered_root()) + # source = os.path.join("{root}", relative_path).replace("\\", "/") + + source = "standalone" + + # self.log.debug("Source: {}".format(source)) + version_data = {"families": families, + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment"), + "machine": context.data.get("machine"), + "fps": context.data.get("fps")} + + # Include optional data if present in + optionals = [ + "startFrame", "endFrame", "step", "handles", "sourceHashes" + ] + for key in optionals: + if key in instance.data: + version_data[key] = instance.data[key] + + return version_data diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 05c889a2d6..11c2f353d1 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -16,13 +16,19 @@ import pyblish.api # Registers Global pyblish plugins -pype.install() +# pype.install() # Registers Standalone pyblish plugins PUBLISH_PATH = os.path.sep.join( [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] ) pyblish.api.register_plugin_path(PUBLISH_PATH) +# Registers Standalone pyblish plugins +# PUBLISH_PATH = os.path.sep.join( +# [pype.PLUGINS_DIR, 'ftrack', 'publish'] +# ) +# pyblish.api.register_plugin_path(PUBLISH_PATH) + def set_context(project, asset, app): ''' Sets context for pyblish (must be done before pyblish is launched) From 484c8e0815e728e530c43e41ed96c50c4378ca56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 May 2019 12:34:16 +0200 Subject: [PATCH 152/193] replaced brackets in representation name incrementing to underline --- pype/standalonepublish/widgets/widget_drop_frame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 0048989707..cffe673152 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -352,14 +352,14 @@ class DropDataFrame(QtWidgets.QFrame): renamed = False for item in self.components_list.widgets(): if repre_name == item.input_repre.text(): - check_regex = '\(\w+\)$' + check_regex = '_\w+$' result = re.findall(check_regex, repre_name) next_num = 2 if len(result) == 1: repre_name = repre_name.replace(result[0], '') - next_num = int(result[0].replace('(', '').replace(')', '')) + next_num = int(result[0].replace('_', '')) next_num += 1 - repre_name = '{}({})'.format(repre_name, next_num) + repre_name = '{}_{}'.format(repre_name, next_num) renamed = True break if renamed: From 5c6bf5fd3b36f2c34c64649290d5ed9208df4ef4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 May 2019 14:02:27 +0200 Subject: [PATCH 153/193] feat(nukestudio): updating wip --- pype/nukestudio/__init__.py | 18 +++++++++--------- pype/nukestudio/lib.py | 25 ------------------------- pype/nukestudio/menu.py | 4 +--- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py index 7a87009141..bbae957f61 100644 --- a/pype/nukestudio/__init__.py +++ b/pype/nukestudio/__init__.py @@ -5,12 +5,11 @@ from pyblish import api as pyblish from .. import api -from pype.nukestudio import menu +from .menu import install as menu_install from .lib import ( show, setup, - register_plugins, add_to_filemenu ) @@ -19,7 +18,7 @@ import nuke from pypeapp import Logger -log = Logger().get_logger(__name__, "nuke") +log = Logger().get_logger(__name__, "nukestudio") AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") @@ -63,18 +62,18 @@ def reload_config(): importlib.reload(module) -def install(): +def install(config): # api.set_avalon_workdir() # reload_config() - import sys - + # import sys # for path in sys.path: # if path.startswith("C:\\Users\\Public"): # sys.path.remove(path) - log.info("Registering Nuke plug-ins..") + log.info("Registering NukeStudio plug-ins..") + pyblish.register_host("nukestudio") pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) @@ -89,14 +88,15 @@ def install(): avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states - menu.install() + menu_install() # load data from templates api.load_data_from_templates() def uninstall(): - log.info("Deregistering Nuke plug-ins..") + log.info("Deregistering NukeStudio plug-ins..") + pyblish.deregister_host("nukestudio") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index e2a11dea08..ca1dfa3544 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -10,8 +10,6 @@ import hiero from PySide2 import (QtWidgets, QtGui) -# Local libraries -import plugins cached_process = None @@ -41,8 +39,6 @@ def setup(console=False, port=None, menu=True): # register bumpybox plugins pyblish.api.register_plugin_path(r"C:\Users\hubert\CODE\github\pyblish-bumpybox\pyblish_bumpybox\plugins\nukestudio") - register_plugins() - register_host() add_submission() if menu: @@ -98,27 +94,6 @@ def remove_from_filemenu(): raise NotImplementedError("Implement me please.") -def deregister_plugins(): - # De-register accompanying plugins - plugin_path = os.path.dirname(plugins.__file__) - pyblish.api.deregister_plugin_path(plugin_path) - print("pyblish: Deregistered %s" % plugin_path) - - -def register_host(): - """Register supported hosts""" - pyblish.api.register_host("nukestudio") - - -def deregister_host(): - """De-register supported hosts""" - pyblish.api.deregister_host("nukestudio") - - -def register_plugins(): - # Register accompanying plugins - plugin_path = os.path.dirname(plugins.__file__) - pyblish.api.register_plugin_path(plugin_path) def add_to_filemenu(): diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py index b62a20559d..5fae4cf2dd 100644 --- a/pype/nukestudio/menu.py +++ b/pype/nukestudio/menu.py @@ -1,8 +1,6 @@ +import os from avalon.api import Session -from pype.nukestudio import lib - - import hiero.core try: From 6868859aeab5bb11d08b2e5af4f2c56901971f25 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 10 May 2019 17:10:31 +0200 Subject: [PATCH 154/193] hotfix/minot fixes for submitting maya to deadline in 2.0 --- pype/ftrack/lib/ftrack_app_handler.py | 2 +- pype/plugins/global/publish/collect_scene_version.py | 6 ++++-- pype/plugins/maya/publish/collect_renderlayers.py | 7 +++++-- pype/plugins/maya/publish/submit_maya_deadline.py | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 3c2bc418a8..5efb968f32 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -193,7 +193,7 @@ class AppAction(BaseHandler): application = avalonlib.get_application(os.environ["AVALON_APP_NAME"]) data = { - "root": os.environ.get("PYPE_STUDIO_PROJECTS_PATH"), + "root": os.environ.get("PYPE_STUDIO_PROJECTS_MOUNT"), "project": { "name": entity['project']['full_name'], "code": entity['project']['name'] diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 06bc8e3a53..12075e2417 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -1,8 +1,8 @@ import os import pyblish.api -import os import pype.api as pype + class CollectSceneVersion(pyblish.api.ContextPlugin): """Finds version in the filename or passes the one found in the context Arguments: @@ -16,8 +16,10 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): filename = os.path.basename(context.data.get('currentFile')) - rootVersion = pype.get_version_from_path(filename) + if '' in filename: + return + rootVersion = pype.get_version_from_path(filename) context.data['version'] = rootVersion self.log.info('Scene Version: %s' % context.data('version')) diff --git a/pype/plugins/maya/publish/collect_renderlayers.py b/pype/plugins/maya/publish/collect_renderlayers.py index d2c64e2117..e494a90878 100644 --- a/pype/plugins/maya/publish/collect_renderlayers.py +++ b/pype/plugins/maya/publish/collect_renderlayers.py @@ -21,11 +21,14 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin): # Get render globals node try: render_globals = cmds.ls("renderglobalsMain")[0] + for instance in context: + self.log.debug(instance.name) + if instance.data['family'] == 'workfile': + instance.data['publish'] = True except IndexError: self.log.info("Skipping renderlayer collection, no " "renderGlobalsDefault found..") return - # Get all valid renderlayers # This is how Maya populates the renderlayer display rlm_attribute = "renderLayerManager.renderLayerId" @@ -51,7 +54,7 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin): continue if layer.endswith("defaultRenderLayer"): - layername = "masterLayer" + continue else: # Remove Maya render setup prefix `rs_` layername = layer.split("rs_", 1)[-1] diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 0a97a9b98f..db3ea85034 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -280,7 +280,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): clean_path = clean_path.replace('python2', 'python3') clean_path = clean_path.replace( os.path.normpath(environment['PYPE_STUDIO_CORE_MOUNT']), - os.path.normpath(environment['PYPE_STUDIO_CORE'])) + os.path.normpath(environment['PYPE_STUDIO_CORE_PATH'])) clean_environment[key] = clean_path environment = clean_environment From 38c693a7925f02dc07f67ac55b7cd6402f73d132 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 11 May 2019 18:17:39 +0200 Subject: [PATCH 155/193] feat(nukestudio): adding otio plugin --- .../Startup/otioexporter/OTIOExportTask.py | 369 +++++++++++++++ .../Startup/otioexporter/OTIOExportUI.py | 65 +++ .../Python/Startup/otioexporter/__init__.py | 29 ++ .../StartupUI/otioimporter/OTIOImport.py | 435 ++++++++++++++++++ .../Python/StartupUI/otioimporter/__init__.py | 57 +++ 5 files changed, 955 insertions(+) create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py create mode 100644 setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py new file mode 100644 index 0000000000..77dc9c45b3 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py @@ -0,0 +1,369 @@ +# MIT License +# +# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import os +import re +import hiero.core +from hiero.core import util + +import opentimelineio as otio + + +marker_color_map = { + "magenta": otio.schema.MarkerColor.MAGENTA, + "red": otio.schema.MarkerColor.RED, + "yellow": otio.schema.MarkerColor.YELLOW, + "green": otio.schema.MarkerColor.GREEN, + "cyan": otio.schema.MarkerColor.CYAN, + "blue": otio.schema.MarkerColor.BLUE, +} + + +class OTIOExportTask(hiero.core.TaskBase): + + def __init__(self, initDict): + """Initialize""" + hiero.core.TaskBase.__init__(self, initDict) + + def name(self): + return str(type(self)) + + def get_rate(self, item): + num, den = item.framerate().toRational() + rate = float(num) / float(den) + + if rate.is_integer(): + return rate + + return round(rate, 2) + + def get_clip_ranges(self, trackitem): + # Is clip an audio file? Use sequence frame rate + if not trackitem.source().mediaSource().hasVideo(): + rate_item = trackitem.sequence() + + else: + rate_item = trackitem.source() + + source_rate = self.get_rate(rate_item) + + # Reversed video/audio + if trackitem.playbackSpeed() < 0: + start = trackitem.sourceOut() + + else: + start = trackitem.sourceIn() + + source_start_time = otio.opentime.RationalTime( + start, + source_rate + ) + source_duration = otio.opentime.RationalTime( + trackitem.duration(), + source_rate + ) + + source_range = otio.opentime.TimeRange( + start_time=source_start_time, + duration=source_duration + ) + + available_range = None + hiero_clip = trackitem.source() + if not hiero_clip.mediaSource().isOffline(): + start_time = otio.opentime.RationalTime( + hiero_clip.mediaSource().startTime(), + source_rate + ) + duration = otio.opentime.RationalTime( + hiero_clip.mediaSource().duration(), + source_rate + ) + available_range = otio.opentime.TimeRange( + start_time=start_time, + duration=duration + ) + + return source_range, available_range + + def add_gap(self, trackitem, otio_track, prev_out): + gap_length = trackitem.timelineIn() - prev_out + if prev_out != 0: + gap_length -= 1 + + rate = self.get_rate(trackitem.sequence()) + gap = otio.opentime.TimeRange( + duration=otio.opentime.RationalTime( + gap_length, + rate + ) + ) + otio_gap = otio.schema.Gap(source_range=gap) + otio_track.append(otio_gap) + + def get_marker_color(self, tag): + icon = tag.icon() + pat = 'icons:Tag(?P\w+)\.\w+' + + res = re.search(pat, icon) + if res: + color = res.groupdict().get('color') + if color.lower() in marker_color_map: + return marker_color_map[color.lower()] + + return otio.schema.MarkerColor.RED + + def add_markers(self, hiero_item, otio_item): + for tag in hiero_item.tags(): + if not tag.visible(): + continue + + if tag.name() == 'Copy': + # Hiero adds this tag to a lot of clips + continue + + frame_rate = self.get_rate(hiero_item) + + marked_range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + tag.inTime(), + frame_rate + ), + duration=otio.opentime.RationalTime( + int(tag.metadata().dict().get('tag.length', '0')), + frame_rate + ) + ) + + marker = otio.schema.Marker( + name=tag.name(), + color=self.get_marker_color(tag), + marked_range=marked_range, + metadata={ + 'Hiero': tag.metadata().dict() + } + ) + + otio_item.markers.append(marker) + + def add_clip(self, trackitem, otio_track, itemindex): + hiero_clip = trackitem.source() + + # Add Gap if needed + prev_item = ( + itemindex and trackitem.parent().items()[itemindex - 1] or + trackitem + ) + + if prev_item == trackitem and trackitem.timelineIn() > 0: + self.add_gap(trackitem, otio_track, 0) + + elif ( + prev_item != trackitem and + prev_item.timelineOut() != trackitem.timelineIn() + ): + self.add_gap(trackitem, otio_track, prev_item.timelineOut()) + + # Create Clip + source_range, available_range = self.get_clip_ranges(trackitem) + + otio_clip = otio.schema.Clip() + otio_clip.name = trackitem.name() + otio_clip.source_range = source_range + + # Add media reference + media_reference = otio.schema.MissingReference() + if not hiero_clip.mediaSource().isOffline(): + source = hiero_clip.mediaSource() + media_reference = otio.schema.ExternalReference() + media_reference.available_range = available_range + + path, name = os.path.split(source.fileinfos()[0].filename()) + media_reference.target_url = os.path.join(path, name) + media_reference.name = name + + otio_clip.media_reference = media_reference + + # Add Time Effects + playbackspeed = trackitem.playbackSpeed() + if playbackspeed != 1: + if playbackspeed == 0: + time_effect = otio.schema.FreezeFrame() + + else: + time_effect = otio.schema.LinearTimeWarp( + time_scalar=playbackspeed + ) + otio_clip.effects.append(time_effect) + + # Add tags as markers + if self._preset.properties()["includeTags"]: + self.add_markers(trackitem.source(), otio_clip) + + otio_track.append(otio_clip) + + # Add Transition if needed + if trackitem.inTransition() or trackitem.outTransition(): + self.add_transition(trackitem, otio_track) + + def add_transition(self, trackitem, otio_track): + transitions = [] + + if trackitem.inTransition(): + if trackitem.inTransition().alignment().name == 'kFadeIn': + transitions.append(trackitem.inTransition()) + + if trackitem.outTransition(): + transitions.append(trackitem.outTransition()) + + for transition in transitions: + alignment = transition.alignment().name + + if alignment == 'kFadeIn': + in_offset_frames = 0 + out_offset_frames = ( + transition.timelineOut() - transition.timelineIn() + ) + 1 + + elif alignment == 'kFadeOut': + in_offset_frames = ( + trackitem.timelineOut() - transition.timelineIn() + ) + 1 + out_offset_frames = 0 + + elif alignment == 'kDissolve': + in_offset_frames = ( + transition.inTrackItem().timelineOut() - + transition.timelineIn() + ) + out_offset_frames = ( + transition.timelineOut() - + transition.outTrackItem().timelineIn() + ) + + else: + # kUnknown transition is ignored + continue + + rate = trackitem.source().framerate().toFloat() + in_time = otio.opentime.RationalTime(in_offset_frames, rate) + out_time = otio.opentime.RationalTime(out_offset_frames, rate) + + otio_transition = otio.schema.Transition( + name=alignment, # Consider placing Hiero name in metadata + transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve, + in_offset=in_time, + out_offset=out_time, + metadata={} + ) + + if alignment == 'kFadeIn': + otio_track.insert(-2, otio_transition) + + else: + otio_track.append(otio_transition) + + def add_tracks(self): + for track in self._sequence.items(): + if isinstance(track, hiero.core.AudioTrack): + kind = otio.schema.TrackKind.Audio + + else: + kind = otio.schema.TrackKind.Video + + otio_track = otio.schema.Track(kind=kind) + otio_track.name = track.name() + + for itemindex, trackitem in enumerate(track): + if isinstance(trackitem.source(), hiero.core.Clip): + self.add_clip(trackitem, otio_track, itemindex) + + self.otio_timeline.tracks.append(otio_track) + + # Add tags as markers + if self._preset.properties()["includeTags"]: + self.add_markers(self._sequence, self.otio_timeline.tracks) + + def create_OTIO(self): + self.otio_timeline = otio.schema.Timeline() + self.otio_timeline.name = self._sequence.name() + + self.add_tracks() + + def startTask(self): + self.create_OTIO() + + def taskStep(self): + return False + + def finishTask(self): + try: + exportPath = self.resolvedExportPath() + + # Check file extension + if not exportPath.lower().endswith(".otio"): + exportPath += ".otio" + + # check export root exists + dirname = os.path.dirname(exportPath) + util.filesystem.makeDirs(dirname) + + # write otio file + otio.adapters.write_to_file(self.otio_timeline, exportPath) + + # Catch all exceptions and log error + except Exception as e: + self.setError("failed to write file {f}\n{e}".format( + f=exportPath, + e=e) + ) + + hiero.core.TaskBase.finishTask(self) + + def forcedAbort(self): + pass + + +class OTIOExportPreset(hiero.core.TaskPresetBase): + def __init__(self, name, properties): + """Initialise presets to default values""" + hiero.core.TaskPresetBase.__init__(self, OTIOExportTask, name) + + self.properties()["includeTags"] = True + self.properties().update(properties) + + def supportedItems(self): + return hiero.core.TaskPresetBase.kSequence + + def addCustomResolveEntries(self, resolver): + resolver.addResolver( + "{ext}", + "Extension of the file to be output", + lambda keyword, task: "otio" + ) + + def supportsAudio(self): + return True + + +hiero.core.taskRegistry.registerTask(OTIOExportPreset, OTIOExportTask) diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py new file mode 100644 index 0000000000..887ff05ec8 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py @@ -0,0 +1,65 @@ +import hiero.ui +import OTIOExportTask + +try: + # Hiero >= 11.x + from PySide2 import QtCore + from PySide2.QtWidgets import QCheckBox + from hiero.ui.FnTaskUIFormLayout import TaskUIFormLayout as FormLayout + +except ImportError: + # Hiero <= 10.x + from PySide import QtCore # lint:ok + from PySide.QtGui import QCheckBox, QFormLayout # lint:ok + + FormLayout = QFormLayout # lint:ok + + +class OTIOExportUI(hiero.ui.TaskUIBase): + def __init__(self, preset): + """Initialize""" + hiero.ui.TaskUIBase.__init__( + self, + OTIOExportTask.OTIOExportTask, + preset, + "OTIO Exporter" + ) + + def includeMarkersCheckboxChanged(self, state): + # Slot to handle change of checkbox state + self._preset.properties()["includeTags"] = state == QtCore.Qt.Checked + + def populateUI(self, widget, exportTemplate): + layout = widget.layout() + formLayout = FormLayout() + + # Hiero ~= 10.0v4 + if layout is None: + layout = formLayout + widget.setLayout(layout) + + else: + layout.addLayout(formLayout) + + # Checkboxes for whether the OTIO should contain markers or not + self.includeMarkersCheckbox = QCheckBox() + self.includeMarkersCheckbox.setToolTip( + "Enable to include Tags as markers in the exported OTIO file." + ) + self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Unchecked) + + if self._preset.properties()["includeTags"]: + self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Checked) + + self.includeMarkersCheckbox.stateChanged.connect( + self.includeMarkersCheckboxChanged + ) + + # Add Checkbox to layout + formLayout.addRow("Include Tags:", self.includeMarkersCheckbox) + + +hiero.ui.taskUIRegistry.registerTaskUI( + OTIOExportTask.OTIOExportPreset, + OTIOExportUI +) diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py new file mode 100644 index 0000000000..67e6e78d35 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py @@ -0,0 +1,29 @@ +# MIT License +# +# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from OTIOExportTask import OTIOExportTask +from OTIOExportUI import OTIOExportUI + +__all__ = [ + 'OTIOExportTask', + 'OTIOExportUI' +] diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py new file mode 100644 index 0000000000..f506333a67 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -0,0 +1,435 @@ +# MIT License +# +# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import sys +import hiero.core +import hiero.ui + +try: + from urllib import unquote + +except ImportError: + from urllib.parse import unquote # lint:ok + +import opentimelineio as otio + + +def get_transition_type(otio_item, otio_track): + _in, _out = otio_track.neighbors_of(otio_item) + + if isinstance(_in, otio.schema.Gap): + _in = None + + if isinstance(_out, otio.schema.Gap): + _out = None + + if _in and _out: + return 'dissolve' + + elif _in and not _out: + return 'fade_out' + + elif not _in and _out: + return 'fade_in' + + else: + return 'unknown' + + +def find_trackitem(name, hiero_track): + for item in hiero_track.items(): + if item.name() == name: + return item + + return None + + +def get_neighboring_trackitems(otio_item, otio_track, hiero_track): + _in, _out = otio_track.neighbors_of(otio_item) + trackitem_in = None + trackitem_out = None + + if _in: + trackitem_in = find_trackitem(_in.name, hiero_track) + + if _out: + trackitem_out = find_trackitem(_out.name, hiero_track) + + return trackitem_in, trackitem_out + + +def apply_transition(otio_track, otio_item, track): + # Figure out type of transition + transition_type = get_transition_type(otio_item, otio_track) + + # Figure out track kind for getattr below + if isinstance(track, hiero.core.VideoTrack): + kind = '' + + else: + kind = 'Audio' + + try: + # Gather TrackItems involved in trasition + item_in, item_out = get_neighboring_trackitems( + otio_item, + otio_track, + track + ) + + # Create transition object + if transition_type == 'dissolve': + transition_func = getattr( + hiero.core.Transition, + 'create{kind}DissolveTransition'.format(kind=kind) + ) + + transition = transition_func( + item_in, + item_out, + otio_item.in_offset.value, + otio_item.out_offset.value + ) + + elif transition_type == 'fade_in': + transition_func = getattr( + hiero.core.Transition, + 'create{kind}FadeInTransition'.format(kind=kind) + ) + transition = transition_func( + item_out, + otio_item.out_offset.value + ) + + elif transition_type == 'fade_out': + transition_func = getattr( + hiero.core.Transition, + 'create{kind}FadeOutTransition'.format(kind=kind) + ) + transition = transition_func( + item_in, + otio_item.in_offset.value + ) + + else: + # Unknown transition + return + + # Apply transition to track + track.addTransition(transition) + + except Exception, e: + sys.stderr.write( + 'Unable to apply transition "{t}": "{e}"\n'.format( + t=otio_item, + e=e + ) + ) + + +def prep_url(url_in): + url = unquote(url_in) + + if url.startswith('file://localhost/'): + return url.replace('file://localhost/', '') + + url = '{url}'.format( + sep=url.startswith(os.sep) and '' or os.sep, + url=url.startswith(os.sep) and url[1:] or url + ) + + return url + + +def create_offline_mediasource(otio_clip, path=None): + hiero_rate = hiero.core.TimeBase( + otio_clip.source_range.start_time.rate + ) + + if isinstance(otio_clip.media_reference, otio.schema.ExternalReference): + source_range = otio_clip.available_range() + + else: + source_range = otio_clip.source_range + + if path is None: + path = otio_clip.name + + media = hiero.core.MediaSource.createOfflineVideoMediaSource( + prep_url(path), + source_range.start_time.value, + source_range.duration.value, + hiero_rate, + source_range.start_time.value + ) + + return media + + +def load_otio(otio_file): + otio_timeline = otio.adapters.read_from_file(otio_file) + build_sequence(otio_timeline) + + +marker_color_map = { + "PINK": "Magenta", + "RED": "Red", + "ORANGE": "Yellow", + "YELLOW": "Yellow", + "GREEN": "Green", + "CYAN": "Cyan", + "BLUE": "Blue", + "PURPLE": "Magenta", + "MAGENTA": "Magenta", + "BLACK": "Blue", + "WHITE": "Green" +} + + +def get_tag(tagname, tagsbin): + for tag in tagsbin.items(): + if tag.name() == tagname: + return tag + + if isinstance(tag, hiero.core.Bin): + tag = get_tag(tagname, tag) + + if tag is not None: + return tag + + return None + + +def add_metadata(metadata, hiero_item): + for key, value in metadata.items(): + if isinstance(value, dict): + add_metadata(value, hiero_item) + continue + + if value is not None: + if not key.startswith('tag.'): + key = 'tag.' + key + + hiero_item.metadata().setValue(key, str(value)) + + +def add_markers(otio_item, hiero_item, tagsbin): + if isinstance(otio_item, (otio.schema.Stack, otio.schema.Clip)): + markers = otio_item.markers + + elif isinstance(otio_item, otio.schema.Timeline): + markers = otio_item.tracks.markers + + else: + markers = [] + + for marker in markers: + marker_color = marker.color + + _tag = get_tag(marker.name, tagsbin) + if _tag is None: + _tag = get_tag(marker_color_map[marker_color], tagsbin) + + if _tag is None: + _tag = hiero.core.Tag(marker_color_map[marker.color]) + + start = marker.marked_range.start_time.value + end = ( + marker.marked_range.start_time.value + + marker.marked_range.duration.value + ) + + tag = hiero_item.addTagToRange(_tag, start, end) + tag.setName(marker.name or marker_color_map[marker_color]) + + # Add metadata + add_metadata(marker.metadata, tag) + + +def create_track(otio_track, tracknum, track_kind): + # Add track kind when dealing with nested stacks + if isinstance(otio_track, otio.schema.Stack): + otio_track.kind = track_kind + + # Create a Track + if otio_track.kind == otio.schema.TrackKind.Video: + track = hiero.core.VideoTrack( + otio_track.name or 'Video{n}'.format(n=tracknum) + ) + + else: + track = hiero.core.AudioTrack( + otio_track.name or 'Audio{n}'.format(n=tracknum) + ) + + return track + + +def create_clip(otio_clip, tagsbin): + # Create MediaSource + otio_media = otio_clip.media_reference + if isinstance(otio_media, otio.schema.ExternalReference): + url = prep_url(otio_media.target_url) + media = hiero.core.MediaSource(url) + if media.isOffline(): + media = create_offline_mediasource(otio_clip, url) + + else: + media = create_offline_mediasource(otio_clip) + + # Create Clip + clip = hiero.core.Clip(media) + + # Add markers + add_markers(otio_clip, clip, tagsbin) + + return clip + + +def create_trackitem(playhead, track, otio_clip, clip): + source_range = otio_clip.source_range + + trackitem = track.createTrackItem(otio_clip.name) + trackitem.setPlaybackSpeed(source_range.start_time.rate) + trackitem.setSource(clip) + + # Check for speed effects and adjust playback speed accordingly + for effect in otio_clip.effects: + if isinstance(effect, otio.schema.LinearTimeWarp): + trackitem.setPlaybackSpeed( + trackitem.playbackSpeed() * + effect.time_scalar + ) + + # If reverse playback speed swap source in and out + if trackitem.playbackSpeed() < 0: + source_out = source_range.start_time.value + source_in = ( + source_range.start_time.value + + source_range.duration.value + ) - 1 + timeline_in = playhead + source_out + timeline_out = ( + timeline_in + + source_range.duration.value + ) - 1 + else: + # Normal playback speed + source_in = source_range.start_time.value + source_out = ( + source_range.start_time.value + + source_range.duration.value + ) - 1 + timeline_in = playhead + timeline_out = ( + timeline_in + + source_range.duration.value + ) - 1 + + # Set source and timeline in/out points + trackitem.setSourceIn(source_in) + trackitem.setSourceOut(source_out) + trackitem.setTimelineIn(timeline_in) + trackitem.setTimelineOut(timeline_out) + + return trackitem + + +def build_sequence(otio_timeline, project=None, track_kind=None): + if project is None: + # TODO: Find a proper way for active project + project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1] + + # Create a Sequence + sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + + # Create a Bin to hold clips + projectbin = project.clipsBin() + projectbin.addItem(hiero.core.BinItem(sequence)) + sequencebin = hiero.core.Bin(sequence.name()) + projectbin.addItem(sequencebin) + + # Get tagsBin + tagsbin = hiero.core.project("Tag Presets").tagsBin() + + # Add timeline markers + add_markers(otio_timeline, sequence, tagsbin) + + # TODO: Set sequence settings from otio timeline if available + if isinstance(otio_timeline, otio.schema.Timeline): + tracks = otio_timeline.tracks + + else: + # otio.schema.Stack + tracks = otio_timeline + + for tracknum, otio_track in enumerate(tracks): + playhead = 0 + _transitions = [] + + # Add track to sequence + track = create_track(otio_track, tracknum, track_kind) + sequence.addTrack(track) + + # iterate over items in track + for itemnum, otio_clip in enumerate(otio_track): + if isinstance(otio_clip, otio.schema.Stack): + bar = hiero.ui.mainWindow().statusBar() + bar.showMessage( + 'Nested sequences are created separately.', + timeout=3000 + ) + build_sequence(otio_clip, project, otio_track.kind) + + elif isinstance(otio_clip, otio.schema.Clip): + # Create a Clip + clip = create_clip(otio_clip, tagsbin) + + # Add Clip to a Bin + sequencebin.addItem(hiero.core.BinItem(clip)) + + # Create TrackItem + trackitem = create_trackitem( + playhead, + track, + otio_clip, + clip + ) + + # Add trackitem to track + track.addTrackItem(trackitem) + + # Update playhead + playhead = trackitem.timelineOut() + 1 + + elif isinstance(otio_clip, otio.schema.Transition): + # Store transitions for when all clips in the track are created + _transitions.append((otio_track, otio_clip)) + + elif isinstance(otio_clip, otio.schema.Gap): + # Hiero has no fillers, slugs or blanks at the moment + playhead += otio_clip.source_range.duration.value + + # Apply transitions we stored earlier now that all clips are present + for otio_track, otio_item in _transitions: + apply_transition(otio_track, otio_item, track) diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py new file mode 100644 index 0000000000..1503a9e9ac --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py @@ -0,0 +1,57 @@ +# MIT License +# +# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import hiero.ui +import hiero.core + +from otioimporter.OTIOImport import load_otio + + +def OTIO_menu_action(event): + otio_action = hiero.ui.createMenuAction( + 'Import OTIO', + open_otio_file, + icon=None + ) + hiero.ui.registerAction(otio_action) + for action in event.menu.actions(): + if action.text() == 'Import': + action.menu().addAction(otio_action) + break + + +def open_otio_file(): + files = hiero.ui.openFileBrowser( + caption='Please select an OTIO file of choice', + pattern='*.otio', + requiredExtension='.otio' + ) + for otio_file in files: + load_otio(otio_file) + + +# HieroPlayer is quite limited and can't create transitions etc. +if not hiero.core.isHieroPlayer(): + hiero.core.events.registerInterest( + "kShowContextMenu/kBin", + OTIO_menu_action + ) From 866ec2e9d2ddb6e5e430f7d815237eca02f74ac0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 11 May 2019 18:18:22 +0200 Subject: [PATCH 156/193] feat(nukestudio): adding basic menu itegration of avalon --- pype/nukestudio/__init__.py | 14 +- pype/nukestudio/inventory.py | 347 ----------------------------------- pype/nukestudio/lib.py | 14 +- pype/nukestudio/menu.py | 41 +++-- 4 files changed, 40 insertions(+), 376 deletions(-) delete mode 100644 pype/nukestudio/inventory.py diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py index bbae957f61..36f3453cf7 100644 --- a/pype/nukestudio/__init__.py +++ b/pype/nukestudio/__init__.py @@ -13,7 +13,6 @@ from .lib import ( add_to_filemenu ) -import nuke from pypeapp import Logger @@ -103,3 +102,16 @@ def uninstall(): # reset data from templates api.reset_data_from_templates() + + +def ls(): + """List available containers. + + This function is used by the Container Manager in Nuke. You'll + need to implement a for-loop that then *yields* one Container at + a time. + + See the `container.json` schema for details on how it should look, + and the Maya equivalent, which is in `avalon.maya.pipeline` + """ + return diff --git a/pype/nukestudio/inventory.py b/pype/nukestudio/inventory.py deleted file mode 100644 index 0d030c64ad..0000000000 --- a/pype/nukestudio/inventory.py +++ /dev/null @@ -1,347 +0,0 @@ -import os - -from pyblish import api - - -# Collection -collect_json_CollectJSON = api.CollectorOrder + 0.1 -collect_source_CollectScene = api.CollectorOrder + 0.1 -collect_scene_version_CollectSceneVersion = api.CollectorOrder + 0.1 -collect_existing_files_CollectExistingFiles = api.CollectorOrder + 0.25 -collect_reviews_CollectReviews = api.CollectorOrder + 0.3 -collect_sorting_CollectSorting = api.CollectorOrder + 0.49 - -# Validation -persist_publish_state_PersistPublishState = api.ValidatorOrder -validate_executables_ValidateFFmpeg = api.ValidatorOrder -validate_processing_ValidateProcessing = api.ValidatorOrder -validate_scene_version_ValidateSceneVersion = api.ValidatorOrder -validate_review_ValidateReview = api.ValidatorOrder - -# Extraction -extract_scene_save_ExtractSceneSave = api.ExtractorOrder - 0.49 -extract_review_ExtractReview = api.ExtractorOrder -extract_review_ExtractReviewTranscode = api.ExtractorOrder + 0.02 -extract_review_ExtractReviewTranscodeNukeStudio = ( - api.ExtractorOrder + 0.02 -) - -# Integration -extract_json_ExtractJSON = api.IntegratorOrder + 1 -copy_to_clipboard_action_Report = api.IntegratorOrder + 1 - -# AfterEffects -aftereffects_collect_render_items_CollectRenderItems = api.CollectorOrder -aftereffects_collect_scene_CollectScene = api.CollectorOrder - -aftereffects_validate_output_path_ValidateOutputPath = api.ValidatorOrder -aftereffects_validate_scene_path_ValidateScenePath = api.ValidatorOrder -aftereffects_validate_unique_comp_renders_ValidateUniqueCompRenders = ( - api.ValidatorOrder -) - -aftereffects_append_deadline_data_AppendDeadlineData = api.ExtractorOrder -aftereffects_append_ftrack_audio_AppendFtrackAudio = api.ExtractorOrder -aftereffects_extract_local_ExtractLocal = api.ExtractorOrder - -# CelAction -celaction_collect_scene_CollectScene = api.CollectorOrder -celaction_collect_render_CollectRender = api.CollectorOrder + 0.1 -celaction_bait_append_ftrack_data_AppendFtrackData = ( - api.CollectorOrder + 0.1 -) -celaction_bait_append_ftrack_asset_name_AppendFtrackAssetName = ( - api.CollectorOrder + 0.1 -) - -celaction_bait_validate_scene_path_ValidateScenePath = ( - api.ValidatorOrder -) - -celaction_bait_append_ftrack_data_AppendFtrackAudio = ( - api.ExtractorOrder -) -celaction_extract_deadline_ExtractDeadline = api.ExtractorOrder -celaction_extract_render_images_ExtractRenderImages = api.ExtractorOrder -celaction_extract_render_images_ExtractRenderMovie = api.ExtractorOrder + 0.1 -celaction_extract_deadline_movie_ExtractDeadlineMovie = ( - api.ExtractorOrder + 0.4 -) - -celaction_bait_integrate_local_render_IntegrateLocal = ( - api.IntegratorOrder -) - -# Deadline -deadline_OnJobFinished_collect_output_CollectOutput = api.CollectorOrder -deadline_OnJobSubmitted_collect_movie_CollectMovie = api.CollectorOrder -deadline_OnJobSubmitted_collect_render_CollectRender = api.CollectorOrder -deadline_collect_family_CollectFamily = api.CollectorOrder + 0.1 -deadline_collect_houdini_parameters_CollectHoudiniParameters = ( - deadline_collect_family_CollectFamily + 0.01 -) -deadline_collect_maya_parameters_CollectMayaParameters = ( - deadline_collect_family_CollectFamily + 0.01 -) -deadline_collect_nuke_parameters_CollectNukeParameters = ( - deadline_collect_family_CollectFamily + 0.01 -) -deadline_collect_houdini_render_CollectHoudiniRender = api.CollectorOrder + 0.4 - -deadline_validate_houdini_parameters_ValidateHoudiniParameters = ( - api.ValidatorOrder -) -deadline_validate_maya_parameters_ValidateMayaParameters = api.ValidatorOrder -deadline_validate_nuke_parameters_ValidateNukeParameters = api.ValidatorOrder - -deadline_extract_ftrack_path_ExtractFtrackPath = api.ExtractorOrder -deadline_extract_houdini_ExtractHoudini = api.ExtractorOrder -deadline_extract_job_name_ExtractJobName = api.ExtractorOrder -deadline_extract_maya_ExtractMaya = api.ExtractorOrder -deadline_extract_nuke_ExtractNuke = api.ExtractorOrder -deadline_extract_suspended_ExtractSuspended = api.ExtractorOrder - -deadline_integrate_collection_IntegrateCollection = api.IntegratorOrder - 0.1 -deadline_bait_integrate_ftrack_thumbnail_IntegrateFtrackThumbnail = ( - api.IntegratorOrder -) -deadline_bait_update_ftrack_status_UpdateFtrackStatus = ( - api.IntegratorOrder + 0.4 -) - - -# Ftrack -ftrack_collect_nukestudio_CollectNukeStudioEntities = api.CollectorOrder + 0.1 -ftrack_collect_nukestudio_CollectNukeStudioProjectData = ( - api.CollectorOrder + 0.1 -) -ftrack_collect_version_CollectVersion = api.CollectorOrder + 0.2 -ftrack_collect_family_CollectFamily = api.CollectorOrder + 0.4 - -ftrack_validate_assets_ValidateAssets = api.ValidatorOrder -ftrack_validate_nuke_settings_ValidateNukeSettings = api.ValidatorOrder -ftrack_validate_nukestudio_ValidateNukeStudioProjectData = api.ValidatorOrder -ftrack_validate_nukestudio_tasks_ValidateNukeStudioTasks = api.ValidatorOrder - -ftrack_extract_components_ExtractCache = api.ExtractorOrder -ftrack_extract_components_ExtractCamera = api.ExtractorOrder -ftrack_extract_components_ExtractGeometry = api.ExtractorOrder -ftrack_extract_components_ExtractGizmo = api.ExtractorOrder -ftrack_extract_components_ExtractImg = api.ExtractorOrder -ftrack_extract_components_ExtractLUT = api.ExtractorOrder -ftrack_extract_components_ExtractMovie = api.ExtractorOrder -ftrack_extract_components_ExtractAudio = api.ExtractorOrder -ftrack_extract_components_ExtractReview = api.ExtractorOrder -ftrack_extract_components_ExtractScene = api.ExtractorOrder -ftrack_extract_entities_ExtractProject = api.ExtractorOrder -ftrack_extract_entities_ExtractEpisode = ( - ftrack_extract_entities_ExtractProject + 0.01 -) -ftrack_extract_entities_ExtractSequence = ( - ftrack_extract_entities_ExtractEpisode + 0.01 -) -ftrack_extract_entities_ExtractShot = ( - ftrack_extract_entities_ExtractSequence + 0.01 -) -ftrack_extract_entities_ExtractLinkAssetbuilds = ( - ftrack_extract_entities_ExtractShot + 0.01 -) -ftrack_extract_entities_ExtractAssetDataNukeStudio = ( - ftrack_extract_entities_ExtractShot + 0.01 -) -ftrack_extract_entities_ExtractTasks = ( - ftrack_extract_entities_ExtractShot + 0.01 -) -ftrack_extract_entities_ExtractCommit = ( - ftrack_extract_entities_ExtractTasks + 0.01 -) -ftrack_extract_entities_ExtractNukeStudio = ( - ftrack_extract_entities_ExtractTasks + 0.01 -) -ftrack_extract_thumbnail_ExtractThumbnailImg = api.ExtractorOrder + 0.1 -ftrack_extract_review_ExtractReview = api.ExtractorOrder + 0.2 -ftrack_extract_components_ExtractComponents = api.ExtractorOrder + 0.4 - -ftrack_integrate_status_IntegrateStatus = api.IntegratorOrder - -ftrack_other_link_source_OtherLinkSource = api.IntegratorOrder + 1 - -# Hiero -hiero_collect_items_CollectItems = api.CollectorOrder - -hiero_validate_names_ValidateNames = api.ValidatorOrder - -hiero_extract_transcode_BumpyboxExtractTranscodeH264 = api.ExtractorOrder - 0.1 -hiero_extract_transcode_BumpyboxExtractTranscodeJPEG = api.ExtractorOrder - 0.1 -hiero_extract_audio_ExtractAudio = api.ExtractorOrder -hiero_extract_ftrack_shot_ExtractFtrackShot = api.ExtractorOrder -hiero_extract_nuke_script_ExtractNukeScript = api.ExtractorOrder -hiero_extract_transcode_ExtractTranscode = api.ExtractorOrder -hiero_extract_ftrack_components_ExtractFtrackComponents = ( - api.ExtractorOrder + 0.1 -) -hiero_extract_ftrack_tasks_ExtractFtrackTasks = api.ExtractorOrder + 0.1 -hiero_extract_ftrack_thumbnail_ExtractFtrackThumbnail = ( - api.ExtractorOrder + 0.1 -) - -# Houdini -houdini_collect_Collect = api.CollectorOrder - -houdini_validate_alembic_ValidateAlembic = api.ValidatorOrder -houdini_validate_dynamics_ValidateDynamics = api.ValidatorOrder -houdini_validate_geometry_ValidateGeometry = api.ValidatorOrder -houdini_validate_mantra_camera_ValidateMantraCamera = api.ValidatorOrder -houdini_validate_mantra_settings_ValidateMantraSettings = api.ValidatorOrder -houdini_validate_output_path_ValidateOutputPath = api.ValidatorOrder - -houdini_extract_scene_save_ExtractSceneSave = api.ExtractorOrder - 0.1 -houdini_extract_local_ExtractLocal = api.ExtractorOrder - -# Maya -maya_collect_framerate_CollectFramerate = api.CollectorOrder - 0.5 -maya_collect_files_CollectFiles = api.CollectorOrder -maya_collect_render_setups_CollectRenderSetups = api.CollectorOrder -maya_collect_sets_CollectSets = api.CollectorOrder -maya_collect_sets_CollectSetsProcess = maya_collect_sets_CollectSets + 0.01 -maya_collect_sets_CollectSetsPublish = maya_collect_sets_CollectSets + 0.01 -maya_collect_playblasts_CollectPlayblasts = api.CollectorOrder -maya_collect_playblasts_CollectPlayblastsProcess = ( - maya_collect_playblasts_CollectPlayblasts + 0.01 -) -maya_collect_playblasts_CollectPlayblastsPublish = ( - maya_collect_playblasts_CollectPlayblasts + 0.01 -) - -maya_modeling_validate_intermediate_shapes_ValidateIntermediateShapes = ( - api.ValidatorOrder -) -maya_modeling_validate_points_ValidatePoints = ( - api.ValidatorOrder -) -maya_modeling_validate_hierarchy_ValidateHierarchy = ( - api.ValidatorOrder -) -maya_modeling_validate_shape_name_ValidateShapeName = ( - api.ValidatorOrder -) -maya_modeling_validate_transforms_ValidateTransforms = ( - api.ValidatorOrder -) -maya_modeling_validate_display_layer_ValidateDisplayLayer = ( - api.ValidatorOrder -) -maya_modeling_validate_smooth_display_ValidateSmoothDisplay = ( - api.ValidatorOrder -) -maya_validate_arnold_setings_ValidateArnoldSettings = api.ValidatorOrder -maya_validate_name_ValidateName = api.ValidatorOrder -maya_validate_render_camera_ValidateRenderCamera = api.ValidatorOrder -maya_validate_render_layer_settings_ValidateRenderLayerSettings = ( - api.ValidatorOrder -) -maya_validate_vray_settings_ValidateVraySettings = api.ValidatorOrder - -maya_validate_scene_modified_ValidateSceneModified = api.ExtractorOrder - 0.49 -maya_extract_alembic_ExtractAlembic = api.ExtractorOrder -maya_extract_formats_ExtractFormats = api.ExtractorOrder -maya_lookdev_extract_construction_history_ExtractConstructionHistory = ( - maya_extract_formats_ExtractFormats - 0.01 -) -maya_modeling_extract_construction_history_ExtractConstructionHistory = ( - maya_extract_formats_ExtractFormats - 0.01 -) -maya_rigging_extract_disconnect_animation_ExtractDisconnectAnimation = ( - maya_extract_formats_ExtractFormats - 0.01 -) -maya_extract_playblast_ExtractPlayblast = api.ExtractorOrder -maya_extract_render_layer_ExtractRenderLayer = api.ExtractorOrder - -# Nuke -nuke_collect_selection_CollectSelection = api.CollectorOrder - 0.1 -nuke_collect_backdrops_CollectBackdrops = api.CollectorOrder + 0.1 -nuke_collect_framerate_CollectFramerate = api.CollectorOrder -nuke_collect_reads_CollectReads = api.CollectorOrder -nuke_collect_write_geo_CollectWriteGeo = api.CollectorOrder -nuke_collect_writes_CollectWrites = api.CollectorOrder -nuke_collect_write_geo_CollectCacheProcess = api.CollectorOrder + 0.01 -nuke_collect_write_geo_CollectCachePublish = api.CollectorOrder + 0.01 -nuke_collect_writes_CollectWritesProcess = api.CollectorOrder + 0.01 -nuke_collect_writes_CollectWritesPublish = api.CollectorOrder + 0.01 -nuke_collect_groups_CollectGroups = api.CollectorOrder + 0.1 - -nuke_validate_datatype_ValidateDatatype = api.ValidatorOrder -nuke_validate_frame_rate_ValidateFrameRate = api.ValidatorOrder -nuke_validate_group_node_ValidateGroupNode = api.ValidatorOrder -nuke_validate_proxy_mode_ValidateProxyMode = api.ValidatorOrder -nuke_validate_read_node_ValidateReadNode = api.ValidatorOrder -nuke_validate_write_node_ValidateWriteNode = api.ValidatorOrder -nuke_validate_write_node_ValidateReviewNodeDuplicate = api.ValidatorOrder -nuke_validate_writegeo_node_ValidateWriteGeoNode = api.ValidatorOrder - -nuke_extract_output_directory_ExtractOutputDirectory = api.ExtractorOrder - 0.1 -nuke_extract_backdrop_ExtractBackdrop = api.ExtractorOrder -nuke_extract_group_ExtractGroup = api.ExtractorOrder -nuke_extract_write_Extract = api.ExtractorOrder -nuke_extract_write_ExtractCache = api.ExtractorOrder -nuke_extract_write_ExtractCamera = api.ExtractorOrder -nuke_extract_write_ExtractGeometry = api.ExtractorOrder -nuke_extract_write_ExtractWrite = api.ExtractorOrder -nuke_extract_review_ExtractReview = api.ExtractorOrder + 0.01 - -# NukeStudio -nukestudio_collect_CollectFramerate = api.CollectorOrder -nukestudio_collect_CollectTrackItems = api.CollectorOrder -nukestudio_collect_CollectTasks = api.CollectorOrder + 0.01 - -nukestudio_validate_names_ValidateNames = api.ValidatorOrder -nukestudio_validate_names_ValidateNamesFtrack = api.ValidatorOrder -nukestudio_validate_projectroot_ValidateProjectRoot = api.ValidatorOrder -nukestudio_validate_resolved_paths_ValidateResolvedPaths = api.ValidatorOrder -nukestudio_validate_task_ValidateImageSequence = api.ValidatorOrder -nukestudio_validate_task_ValidateOutputRange = api.ValidatorOrder -nukestudio_validate_track_item_ValidateTrackItem = api.ValidatorOrder -nukestudio_validate_track_item_ValidateTrackItemFtrack = api.ValidatorOrder -nukestudio_validate_viewer_lut_ValidateViewerLut = api.ValidatorOrder - -nukestudio_extract_review_ExtractReview = api.ExtractorOrder -nukestudio_extract_tasks_ExtractTasks = api.ExtractorOrder - -# RoyalRender -royalrender_collect_CollectMayaSets = api.CollectorOrder + 0.1 -royalrender_collect_CollectNukeWrites = api.CollectorOrder + 0.1 - -royalrender_extract_maya_ExtractMaya = api.ExtractorOrder -royalrender_extract_maya_alembic_ExtractMovie = api.ExtractorOrder -royalrender_extract_nuke_ExtractNuke = api.ExtractorOrder - -# TVPaint -tvpaint_extract_deadline_ExtractDeadline = api.ExtractorOrder - 0.1 -tvpaint_collect_scene_arg_CollectSceneArg = api.CollectorOrder - 0.05 -tvpaint_collect_render_CollectRender = api.CollectorOrder + 0.1 - -tvpaint_validate_scene_path_ValidateScenePath = api.ValidatorOrder - -tvpaint_extract_hobsoft_scene_ExtractHobsoftScene = api.ExtractorOrder - - -def get_order(module, name): - path = get_variable_name(module, name) - - if path not in globals().keys(): - raise KeyError("\"{0}\" could not be found in inventory.".format(path)) - - return globals()[path] - - -def get_variable_name(module, name): - plugins_directory = os.path.abspath( - os.path.join(__file__, "..", "plugins") - ) - - module = os.path.relpath(module, plugins_directory) - path = "{0}{1}".format(module, name) - path = path.replace(".py", "_") - path = path.replace(os.sep, "_") - - return path diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index ca1dfa3544..fba8572235 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -36,9 +36,6 @@ def setup(console=False, port=None, menu=True): if self._has_been_setup: teardown() - # register bumpybox plugins - pyblish.api.register_plugin_path(r"C:\Users\hubert\CODE\github\pyblish-bumpybox\pyblish_bumpybox\plugins\nukestudio") - add_submission() if menu: @@ -79,9 +76,6 @@ def teardown(): if not self._has_been_setup: return - deregister_plugins() - deregister_host() - if self._has_menu: remove_from_filemenu() self._has_menu = False @@ -94,8 +88,6 @@ def remove_from_filemenu(): raise NotImplementedError("Implement me please.") - - def add_to_filemenu(): PublishAction() @@ -129,17 +121,13 @@ class PublishAction(QtWidgets.QAction): self.setShortcut("Ctrl+Alt+P") def publish(self): - import pyblish_nukestudio - # Removing "submission" attribute from hiero module, to prevent tasks # from getting picked up when not using the "Export" dialog. if hasattr(hiero, "submission"): del hiero.submission - - pyblish_nukestudio.show() + show() def eventHandler(self, event): - # Add the Menu to the right-click menu event.menu.addAction(self) diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py index 5fae4cf2dd..b6e17aeab2 100644 --- a/pype/nukestudio/menu.py +++ b/pype/nukestudio/menu.py @@ -1,5 +1,6 @@ import os from avalon.api import Session +from pprint import pprint import hiero.core @@ -31,9 +32,10 @@ def install(): # Add a Menu to the MenuBar file_action = None + try: check_made_menu = findMenuAction(menu_name) - except: + except Exception: pass if not check_made_menu: @@ -42,40 +44,49 @@ def install(): menu = check_made_menu.menu() actions = [{ - 'action': QAction(QIcon('icons:Position.png'), 'Set Context', None), - 'function': contextmanager.show + 'action': QAction('Set Context', None), + 'function': contextmanager.show, + 'icon': QIcon('icons:Position.png') }, { - 'action': QAction(QIcon('icons:ColorAdd.png'), 'Create...', None), - 'function': creator.show + 'action': QAction('Create...', None), + 'function': creator.show, + 'icon': QIcon('icons:ColorAdd.png') }, { - 'action': QAction(QIcon('icons:CopyRectangle.png'), 'Load...', None), - 'function': cbloader.show + 'action': QAction('Load...', None), + 'function': cbloader.show, + 'icon': QIcon('icons:CopyRectangle.png') }, { - 'action': QAction(QIcon('icons:Output.png'), 'Publish...', None), - 'function': publish.show + 'action': QAction('Publish...', None), + 'function': publish.show, + 'icon': QIcon('icons:Output.png') }, { - 'action': QAction(QIcon('icons:ModifyMetaData.png'), 'Manage...', None), - 'function': cbsceneinventory.show + 'action': QAction('Manage...', None), + 'function': cbsceneinventory.show, + 'icon': QIcon('icons:ModifyMetaData.png') }, { - 'action': QAction(QIcon('icons:ColorAdd.png'), 'Library...', None), - 'function': libraryloader.show + 'action': QAction('Library...', None), + 'function': libraryloader.show, + 'icon': QIcon('icons:ColorAdd.png') }] # Create menu items for a in actions: + pprint(a) # create action for k in a.keys(): if 'action' in k: action = a[k] elif 'function' in k: action.triggered.connect(a[k]) - else: - pass + elif 'icon' in k: + action.setIcon(a[k]) + # add action to menu menu.addAction(action) + hiero.ui.registerAction(action) From ec491113026402da351faaf6775d2813eace819d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 11 May 2019 18:19:15 +0200 Subject: [PATCH 157/193] feat(nukestudio): adding sharedTags.hrox for unified tags to use with pype --- .../Templates/SharedTags.hrox | 468 ++++++++++++++++++ .../hiero_plugin_path/Templates/fusion.png | Bin 0 -> 194142 bytes .../hiero_plugin_path/Templates/houdini.png | Bin 0 -> 11586 bytes .../hiero_plugin_path/Templates/maya.png | Bin 0 -> 13120 bytes .../hiero_plugin_path/Templates/nuke.png | Bin 0 -> 65305 bytes .../hiero_plugin_path/Templates/vfx_aces.hrox | 38 -- .../Templates/vfx_linear.hrox | 38 -- .../Templates/vfx_rec709.hrox | 38 -- .../hiero_plugin_path/Templates/volume.png | Bin 0 -> 19442 bytes 9 files changed, 468 insertions(+), 114 deletions(-) create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/fusion.png create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/houdini.png create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/maya.png create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/nuke.png delete mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox delete mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox delete mode 100644 setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox create mode 100644 setup/nukestudio/hiero_plugin_path/Templates/volume.png diff --git a/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox b/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox new file mode 100644 index 0000000000..4045ea3335 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox @@ -0,0 +1,468 @@ + + + + + + + 2 + 70 + 0 + 0 + 13 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 70 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 70 + 0 + 0 + + + 2 + 70 + 0 + 0 + 17 + + + 2 + 70 + 0 + 0 + 2 + + + + + + + + + diff --git a/setup/nukestudio/hiero_plugin_path/Templates/fusion.png b/setup/nukestudio/hiero_plugin_path/Templates/fusion.png new file mode 100644 index 0000000000000000000000000000000000000000..208c1279cf461400cf4a03262a5bca30ea4b90c8 GIT binary patch literal 194142 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
5B1`Z+kyd14CwS0zK7trii8jHz*> zbYfF@_MgGqvKC|WaM@wI1eJXQ@hC^`GVbpk`c^kVe`SxarqeTBWDo?p*g_%!VU_9F zY}WEHh8bk1-mk*ILtfBsr=|6~sZ}8itx5w8+Bd$H{h4rk_LMH^nR_rzIm@rSZnIkS73q-l`yPaF@y0^f*-8o6Kuk8V+UE9vao0O7Xz~J>fu=c!v=Y>GnG5kVuF@;L5ZK@^xkP?0h5e`oLgO`u z1e1ix?tLaTdq&y$ZtFJh&D(3e5%sh6LSxUJnROkhLhk%4@-Tg(MO;41{ErKe)hDr6z``a__B2pIXA!bC z8aanJbn065F%5p^AogpL;$n|zb4aZ@Lzk7uYBZ6##&3kC$&qR}2uvsPSX3$2fdt0&*VZV-Lw{u}xG!)@2zdyJxS!hU zOCKu|(Efs`Z$HY%&)1r)u~JvQizx?x^Yggn8>gp~wsg%w5_^Sn1wr<$AT}C3PY3%Yb;foPC?XII1wGbrd|3+icP9-D`31d=UJ6vU`&@ zQ3j!+nB_hVoQ)T1@}%?Wm_L*LV~#+znRhwN9@+dTCgdk_`-kAJ3a@qV5%e8%ggPe@ z0`CC1TgGDIBVVLx;;h&nej-(WK0?2gIwygw4QBHn8$yEi_#lT_Um`kjf4`{PXI4SU z;R}b$Kzy9qxQCw#Y%}v2G!P~QU8t6&h8n)Bv}DZhFb7|+|DG?s`AB~&N7EIpq5Och z=gq@J;v=MUlT_Dct$qWv0)|NnWkHi$z2G92hBPCfoPF^zPNHvR-xBH`(ggj@7!}0W z16oW)Qi7$rJt6jNWf6PC3NFI6(I5CLaq4y0m`w>@7S7qNVSk=A31|2r#+_ItrYyi) zZsfH9wdejL?olcRB}PTTUxR-p$L=>6umvtA0DD{s-|{F&4JdpePA7%V<Q9aon{0y!~{& zT}Vp3GaBu;by(Bhz;06@Y~*aq)wt7e1f@>&qkCikFe_mqrzOulEW$3pIF;Aif0qU2 zoyvgI8z^uNPk{j+Mz@Ux|MJp6K;G}P(a!ww*81$d55#ZkE?_$9stkf54^{rARHCyj zMP>$BdJh%)_JnRKt1od!slp7<{)8rMK7qtb+e(lmjACi9eCP*>cYT4jt+8JcBAkz{ zVq<1|0C_f?N>uJYtO?^j(lcl61f^;H-Ftd_AEDu!0;A8##{x+!;%{Wi!D<0+{pj-A z_Vm|XjRV5K{m!P&ASw3iWu96&dcpWoE6C>m?_Y?*_^g%nZKup@+HbFkS!VQw`0G|D z{QNYJu_-4L1{6s$%vBboI;@tC)|%fTZHbIaJ{XuR1C{TN{_ZOfo<;uyy~A6^Q|O-L zN!C83C@%bV%R6OwqQZe=g6o^3UfeRC5(H_k!FZ1;sm_ke<3=p z>ss8RTxpxR0)Rs4|4dn_zeafYe7(7I#6U;Bw*_!*w{pIcNfOH3UOq5npN~0cg z|v`^I$L?8oN+zr$r1Co7t?Oh)H87noO?1)eMNm`C%3M5tyKmPzF59p zc3_)y6s!^1_NZUpFPppw^jqaquu)!e^@ZW|gz^1zHse5iTyox8da1B->B4K_iM#N+7b%PZ3Z~_9}3l-!pjWOPc2K zC7^#2vWU4Q7WZE{d*8 zA4b9%((;AIjj?(eaP@`C@W~$Hk zOINabOWZGJ83;Y0N>kOhA=iNez(wo>WO>nr!MPDq!D`4>s?QZT4R(0sEgK_=8V;wa z56dhZ+(Y-y2#RlGaAY8g!v+?>xs%ni=}QX2UoiMwd39Rj{Ynur!nMf}PbQ1)DvaG3Pq7=FBxu-Q$r*4%i&nYVWtTU_Rc z%q;a@A+?uHc5G}c-ye5$$M*Poo;^U|YPmBZjuuQ;YStvTyE%{3?sET?z{Y|jhkA+{ zd8`J2M&h$#YyA7oY0irzX(gkwKDtjsoqu{}qxw_m2`~5Xoz70&_VzJ#U5-VwFHA1| zNN(X-l0o{8xtM)oO=BiP)`|J*zbOwAcu#)p+%8b4AAifr2O-7uMvuT!R5$pcDx38d zF7Hh*VqSW0Uf=}3lulcRv9w>B>tp$)2Z@5De>_r*<@l zJNVp11adHP!M`8)Dldh_*wtGTZKTkYG$shqoyoW2RG=YSkn<=3hPm7vmnenkvt(rW z!UDVyu;|>eIIyr~cx`Rc(tEC!%T=h8HKKdf-ww=*7(~#Ff#XvSE4y7F9p0j&2jN(A zZ*`BmO6zhQQKBH&vC!N=eeSOmv(Nl)U;iK1VCr1jEX%D9Bw267l34X( z5>0&40Hm{gN$Hee z<)JJ-Q-u3jrOg`zZTH-w{~rJxLF2xE`HdqSnxDbZi{pV1?@g6hnK_Y2YzI{w%8+r6 z#rox?Iqoi({PgFidG)uyCu=@~i6clLK82y`BR1zhN+c4aY`OHDRsCvwQyJ`5jp;)5 zh*rkCc-(r=Yu{~GdEIR&dTqT54Ptl=^xUY9QT64JfgTS@O{?Rb)Q|V;K}>Vni9{lC zfT&{)3WZ#YEWs?Amg`uL&-r3zk##+k!y zgADxW@w+;0ZNi2g`aY3JBpx3mn_Upi8^fQC80apWT`)9{@5UO^L}I(CXZo$0S+5Lq zQ3%0MZE^GN9bS9$0)PEir)U;Jp%A5_4Kas4ko*zDbkmelBC!EtfUjEl!GzbSmV;6T zxO~0Gk6ymYho3CbnthI5&^WIqp6Hbeads??qZ5fOv7QJ0(NwE7Zm%}d2^iXUzKCUm zF!hnJa)zGe+VdW$W7P*~;x@L5puts!ceS>~N-QJB`S7+M@SseSXWoK7V6lED*QPrg}gRNLU0H>__**TI&fVvOU$tHZhD{Y3_!LF|)F z$b*b*D@NyxhP|7Bmzdoib-%`P&a0HS{)Q}#L=%a`V?@7w6Ab^F^w8y#=ebQ!@-OPDTULU6IiMBeX7{1@l>R!-S z2jK|8fUJ^jtH(-lpFsnbg=(U${>sN~60d_JJ|kv=SYFklj+>9we!>HFsntA2IWlfk ziTP(C_`Z;9Eaw~I~>u*`FV2f?q`|Q|7q9Xso#Gm`p=!X|JP%^>sQ|!8*Ojm^GhXkEryA6E}vT*{(iJ5dNzN^M6=jj zS~aaX_3wyHyHILJr zV21Z4ijilEN+8RWB2y%bf9E;4OmNQOG=|Ivi-vp%fwC;=^?H=0rxyet6d#~0jnV^T zpgF#$sLN5!fD+X*jyNqQxQSH8x>*(7Vu(Jz)9yPF0gy-}4hEz08%|v|w=n$usDYl( zEg(89zq!MQmw2)n2*<>Cf(FH*;7bY>N<2ZpbcQ#7fA#-o?>*n_xUPH6cdcDj=X694 z1Oo`>AO?vPDN&J&$Z{u*S4-L%a*K2QWU8~iJ~YH zB*h341POp3GCG}8wfCA2yXy4mMg!{?mtH;8CYu!thy(0>$g7zx7*^I!-=J!ws zT>4-HZ&U%QV2q6ijvhCT9q)4N#A)6=Ho==mDh|Cj!SQ2fICHwg#Ka_6hI2Vp6_1Aq zqH@kNHP;!Ho^V~$a+!7J`p@yov5>srv@TC1)*I8~yNEc=2G4DvyL@02#03v((eS}s z9l97;tcvD*Js67>PFO4}EnKjW_b;bc=zPcO7tC1D`Lkv!ZB$m z9xWixDT;vk*k5Bw#5#9f=+lr0NQeRN^A##vim`6?*_o-@!E@8ZWcR=L_sjA`VW$!qS9`_u1Z;AtNA7lT4BOE$>ngj2gVr(2H$A!EY!e=>^ zf~X@?7u!>>9TbTTtaIYgxMmgWqEx7X;xN-#b3%-u9+QBYoAoKZm zvCLN_a5c;mYh_Ma3g&BnEY)ZBfA)OezwkL#AmpeRK`N(N>wE@!dsxLOVOs6e$T56W z*IvQh#I%0m%y9*nf)eeD@h{z43mEO%A6ruW;IQ|0EJK zsdb*}zXFEg1B{i*_kR3)9{c&51h;`AS3)&`k^ymX+_NS6Md2)!YV%0Zra<`6HxI>m;&M`#WBm>V0kzE`Vh?_MI2I41(+0Osoa zaZhO(2M%?4;-`l>aaUpd5Us~9NT_$s6f?!dBRYw> zF@%6IKp9HZT39KBZU=0P(cwvMyknU6z3*D?yz?4%Upc~t!GcyH47S0U$4S)}Fz=9h zqirSktc8H1gE&v&z*>%gvLR$_+R);Tn}rANAL7`F9lW{!3@^QUf|p-@hy8Dz;?ybd zp-m`;Fja5?oX4a>To=V5VTj-h0uGT1q>D*Yf-XaYL#UY7W=bNFSQKflJs$$7s%sC4 zs51+Vv}TRwg-O;5p{{K^>&=n|vBZjE_+VuDAZNy=_}=$k<;tD6GqgLS5FZub1Bfdz z6ew1@!HLAGMw%$r+Ql{H;n0bS@BH8hufA$z#pbC05M1p&KYI?CD_lKAsw5Kg!7!=n zDhOFGyj`6EUChe5Zw^8*d4={MB?I^ zUOx$fYS9wS3-g^x=rhn&^A?h|%tD=eE)f5(LmJaSHWx{8b9oTWqF7CUtk}fwo;}J_ zPrt{mE3RO)4USsnwYg_kxV?$Q>cJdNLpmBJ+8r3<(^2VDUl>w{Cq@OM9e`8J{KLV28l)&zTQY;)he*KptaujaaI zM%Xx@47VL>-~>X@+H0%`5=$Y5Ixx~K=DzpD-c~ZwOi^h=1u=>sAPQ=}x88L1oZiZf zk<7DoBxn1k7B}D2=G53#Joo$4{Px*{?0w-~4j!7K(``XEiewpDOcAn4ymsPxz%tYn zHAAh6Q3;a82%>J9ap_pD z4iUV+oN<|7^n3VgSGXvUfdqyIDq8Kp=FN_sS8V1ZAKJvB!#A^cZ-?JLeUKM^e}bbYJ75y) zzqf@Tfj|j3R3wJ@`zU;uR+5Rt+Ge&u^*_tAUI;A zfdlh6@u+mkTgph0vw671o?R_I{DC&lzkHg9AAf<@-#E#^BOTOlMDrYVQK-&$17%9XBIG#4VWsI}Tc#*d{K{}#i1*0dKg*qZZovE#zIj(efcSm^ni zZ<{Vv6@uCQYrgmSg^saUyKuf-s{UtKo#JDlSs{}5@^kw==mnb3k+`b}7Ukg6UgHds z7k?S4RamhK6G5_JI#VNj_xsOt~0BxKO72;~GDHyAhHy@dxq zeIs|>HNwt~j*U4utq7i&OD$4|XDgvmCl`;>A-bKl{^Mv$o=ww)b+2d=dR-wX^x6^~ z{e8xH^G8Gdx~9DaNXy@_L23oD z3POnYqZ(2O&W1RA*emQmIF4&= zC(8m791=3j6r7Q|)WTSw}nXLtM<=lxi3? zA*f+>2;u49=ii=4{I}1~Ml`%Cni89HaYlr#N$M51(AK$_?R~8Jng`95m6q5lN z7DCVa8@M@sI#Bd>?7HGAV(RRj;O)YM{;Vxc%v&+Mezr(M7cYwDJodDM&%iH8xk&N;_QY1kl zv7Gd$c%92Ymn>J!a;eKsn8XEKtiPFxtii%2RqY+ek`JiHPTO;Q4L(u-VHO%yYt&*LOMUxb_6U)Wr zJ^$7D=K!XN=DA3gH_!HY&^34Dxl}HSmx$FNQ+2J#zOntHo3czs zU+no?%8>kph~&+Fe7QH47m`yxAAvl8x=<~y$)+}q)}06@T`W{|y5Rf-_unCW?lZS@ z|NU39ZCi^X6Ed;r>lQ`O+9jHD23#;{`lO)BomPSj=0UWmxw>g!k0^&MvmMJz)pa?v>r^eh+2a_Q~7 zO7y-+lYh91r7AMq@#hq|z zX80yR@a)Mwky!gQ3&dEhW7>hHU`8dvvC}0_{PHNzJ%0?-VO*Bs)M3Fe3)8#0%=mHL zv69ANVtu0K@KuRr0-8};g=9boIG+(ZB_pj#KK#HI{_;z=anrRM7~RkqhTs~7^^x>Svv^lX!BVQ%j{ny=BfjQ7)1d?JKKy7Z)f`zqdfMr{rvEempF2K z3SVV}JVP@BRXU+dmKUhzcwb`Tflx;vXiPmY7uUxkGb0zyLkc5LTuxky`+d##pI(Sn z?Ep7LBs+%(U9w!%<$do=tN|{kMdkEvHN>juo9`I<4$GXbBDjWu;!ekZr&F_x!jc+{7(-i(kFPsVSrE3az0* zeAZ91G#ZO?B=MP*`(wMY-Tu$eDy?)mju|n!{5jU3y-DM(K z7E_aEpNM8BbA6tm#2UaF0i3uS9`EW3s0iZlWaRBF?0@Sh-~Qfn+m1D+NWM9iuA4Xg(vKCT@I4-?%`vI6ztlyiH#!# znGj#!SS%t033VWFGSCz2jOirJdJ-_uiVES4cRc_4?YH^v!>?i4FfMm9!p~Hfzb~w& zv1~Q9WjVOqzVG>1l5=5-(5CVpS9p9`QcX^=YipY?f9@v!=C5z#eK&4k;{ZtTh>Png z5sOJ&(w}?(|<{L69J*1e|sl8HSr~+|2Fo+rh--6o=kD!o*Y;b#2sg zoH^o>Im1Nbx>_1Tz4=e0%KkhI^wjqfmlKP<-}~1o3;C?o+s~vPC#3hs@c#7SC%)Mo zfBRMFFG4Ar2`)1OdO3)7F5?pJ;?7qXU^TKBfs3VPP466C6w_JbSiceB5y?Ou-W-~Z z;)C(JhRRH9WX)$Wq_;` z2oq!6bXCruf9-A_xPKd0ZEI1800GSdCW0chk=K+?BmSMd=S#}$@lCWgq)o`O7S4=f z0}Kom{FlGJp550MeCsk`sZbq{% z1fmh2NF*)_GsAn|6CMlRLpepRlYHpjZT$6LUC-@%h8f5}qvITLfJhy|1=H)Q+9+S6 zM&4Q^#)(?IHW16Ol95@(rooJl+|%aD&28GnVSe`Xeol{_VPNP=%1V)}E`is-J{BWg zyF?-}AH-%^)q?hW(CheKk%0|2+%>xM<6oVwY!L?=slASIAUnIh_F{HX(hf{wzJxlm z-ReBAnCsW%qtsHWb?#Tp0TWuSg4bT%$LZ5&xaF2>7#bB3&8^8fKSw`F-DgRO$pu>Hj!)~;K!$}J zCrYNUb*pg8?K>FRw1oq2AK=9CGl(k?b@jT#*DGWfORlZa3zf#_#6n2-%i@tN1Bnvr40m;dlu{^YY)vS&wKLai`D-o?8v!A0?14Tdv8 ztoE3Pq}aX^hzT(jefr#&Rj3sK0(oB1&MU6imh%UnzKTEl+CAKM>qdNe9A8e z+RO$l$G5y(spt4+jM91mY#xs0UQCm63lRUMK&ay7!Q2^hi`E0 z)mQQH`!1b1<|CG+p^3!em^N7O1ww|C z4x*D}tqgI)ynEyf|KH#JifZD0eD0y03=cSR?M6d9Om@K))4(dfTkeXj7`-&L(O3Z{@+f-9VIWfkS+l0UP+MV3m#)XD@^9aHneI5`1A~|=G3(Zz@;V}~sd3*Z^oM5YZOqpR zwvubLwbD-)%KBUXu8!khiu-4!&)>2k>W~;B9COfBv%yWberROd2R~~;`n-rR9pAe; za%I*sYvpFTP&*g<{KdX@o}Y+X%m_0Hm?;)=%p4+$C>%U=ki$pD*}P>FJGTu|xcWKz z^Vs|Ed*=8#kyt*gHqQmEg{7KjD$oiqN;fF`-kIWC-+7aN{?}&^w;gm8$=Z1HpcT?n z9M)J;=n~x0i|x~EsWQk~8PgZSa(<_^a{huCrFfiX&{h2SB)fMy{@Z{3FrWU|Hnwh1 z)G9Yts^i~53P(tY9Fg9##aq~@EFbWPGKF!IK9kMKoo2ODoHpO>>H1f-h z$gS2!{Ka!A`p>zXPHUyFE|$w_{9Ehy&wM^}rO)wsnVo_zFn;vuhbK?I^b{6+*7tOB z5hm6F0nxIq66kO;L@)?cm@7dImu=7dP%1VQ|1uEfFeWPJ0fGM1uyR zH$81qC02%5Gr10FS9+QYh~n{d%s6`5^P|TO@tyC#OmJIJHwc;$q(of6n#G`HbvYoV z@$e)PiMeS68G~NGvC41cC~kkn!#@ z<@=8u;_LtLG-oDSWCMesLePLoiPia#(kls+foW~Dr}1vBlIE|a$IN=?)y$&Us1{h& z!lpcfA z#_ru4$=m?@_PxXSm?zH%F`pw+#PM4KLjQPcb>MsIuZhIvi1qjV`p5C3Pd_qw^7p^O zg73xOh^Xrg8<)`pZ)RuxNvvgJK5ihxv;-YTWCB%>l7qGJK11^yX$|q4=T7nW|La-y zy>o`~GNThRyk)%-RtV8ZpQ{{OBC$-U7%GO;p^+wmN=ha@oE%er^0VXo+qYlk=!qOH zhA{D%c0r~P=^%7txhHA^9l_G((wZmDUrUdw62f%ou~P?Kf$=FuTc`QLAMWOpAKSu~ z4T2*e7NF60n2~;xSTDrLx-v!{g{EputSFNKZG|f~=X~xnSMt?AzMZx^O*wIb96^Ia z>J8CI==+mymPF$6WsVKHvqj<{86S!DLLiHwz!H_%V>lXM8H&SO1scfOITZy@JoOgV z>hk3;+|K=XZ-rdRMNte@iHqJ8i`Q5;F-Qpc;)SGb3|Kw>!z88|N?n*Y$aT$SqYQW%5!8rP%nO|5ND#y%mQ9m@i6>Yf1h7_)=@x$a%lDW# z^CFvv@8jm{hjDEPR+862X-bx+(!`1(GzLS32TBS~j>F@>ILg2L`%Aog+|e2ufvm)W z^$EZ=sToaJyVQ?2W!utSy_QMy*V5CR(+~nhQ7|zvPG%>#@9qu!(PwvX<@Pp(2Gp9u zv{Gw2NUc`_sp`ji!}Z2sv<^)dqY--(1ZRP^dbVxM`1~L4q1&D0o8NkltaUZm5F)d~ zSyMkvBocE8Qb(E`^lWg`Z66-q`hm}2GIN5z3{~9I0%t8_m{!pau{Pp|(9l8wbzMw4 zhy+9&hNE&7E+4@B5C`|4;^5n77#1$D-aw&3CLYx)mOZqLXq?Bbvq;KAUsp5{66!JO z5+q<`7+GCfFOH>R4$;8y#*AyP-Nex&6>lFrN$Ei|ts|QRMG-XZk0uhAE!Ma6lHOWu z{Mc_Fojm#Cle1LZ&lZI{>$!C(^e%IK-?;`&tLs=ci=WxQUnI?L{d_;C_xkvzbG&Ho z$NR-^8fNV3p6|HxdetRl-sj&?Yp!b*1>O`wUH-;uZ(?1r3N^t8PhPaaHuLlgM;V(s zLu>d$+;QtLn?{X7CI}e9Fo6Klg(h^K0yQ^X&eVuI?6aDc7R`ydnf7AG^i{(s>(mT~ zSWc(}ae?4EbgbainB$3G9OobZ<#`Sr4Y+|Jyo+%jSV6FuvmVFnIw!WS;|tBt)M~=h zzbBTTIj5Qhy?>tb*E(gP^XH8B0v^Lzh6agqiU_6^-Owd>Io;EzxNc{~pMU8VZo6Rv zEhl7-IufO+gwP=L%4a3k9}*YPf{Q}3L@kf_9K`FcWCBA3uHD||ufB2%W2dJ0_1+Ws zVl(DqM4yI0?u61R!b8U)R2qzNol95BjYa@SvZgg=-y|nn48|2fae3J)a%Zi`A2Ydav3Crw%FtL(IGge&eSUcbOYu&QY z`LmxmAHbEUNwIc0b6F7&grE%mJw<{O6@@Te)gShMzh^<^mO6LW*2kQ!I9@ zUh8B1YHa^C;sfUE{^u%!c&f6*stG>vu|0h4 zD>t%hM?3bdm~6Z%7N&{SiXmdSOb7w8%ri1r@u^Q-&0qfcy$rX;m>fG9y>ZT^9P~sY z(T9PBW(Rm$#oa7~CPO!|#+hqp*7^hqdKYx=Qt;Guh*8Xx$``B+LT)KSiRQA`qkL}NVA465y=tD z5CTJk%J!YxIeN6>_1BM+7o$-Uhp_7V`^@#@^0&2rp1LSO$VILn&*jKkuJ>PR<`R&e z50~ou&wl+v+qTr_^-^5Fei^)84mxn=y{EoEdGh6_AcU;5M~e z@Zba2a?c%uY#Qj0TZUL(@1dnJJ#k^8USKU4Xi-cq6G64Z&ASUe`>EZ$_`)IHJJF#% z;;77{s%IIGtCf{*-(rrx)a&<^e*a=Qm#s8kmh1apOuqkeu=wN5>horpRepAeyO{-_ z=AZS#xrNcr=JJ)4GQbN6Qg1#5qDMS-gdhQxsPw6>MOC$U_T`hj_U5C!y6;xL@<%sw z>-8HM%8kMa5)=u2N{@-ec@X0LB5HWLc%b7m4j(J|_J6#{!;iksTW@#CifxF?G4-)B zLGM1bIzln3C^QDd>R#R@5~~CgDlg!sP?sSd9=LB0fA-})T)7$A;*k&wY>K;l=gGtZ zu^3fnradh|!9kTVTzEeG!A_UDy5xaL^h%l`W;g*=5K;1htxS|#`O)JC`M_6P$KOTOEZ+-VwUVAIx+B?vqjXFWmy3(%Us3SHkdT9(L z2=%=@fA4vdQ``S1k+?ii)D?)r*!a6_-gt)3J+z%0uTfgTBfbTa5mfN0TO{U1?HsXb z2Y}Z=z)%XZkh5hd;}1T01s}MppxZfy_ns`vR*f=tB9T}*%zW8|viEt=y^rAzE7-C` zVmT2bNJ-2A^Yuf5nSjN-a@Cmjp=Jye4kJTsgz;{NpFa662i|^^&pvoFUwG&SuG-o{ za>zu^%0W*g&O!o0mk=^eoiUz$Wt@Nf=C642w}9V1=s$sthtCH~_1aMiPnhF)a{l z5-Yn1v4pFJ3`a&`!gW`-_)mX%D=)u&ngfSRvMlR`8!uHlW+IVTdswJ^BjfIx2iwEf z-Zr}PV_&5X_mx;Ry?(Y1Z&{taq>Oo!kAo!9j z%TbpjI>^bBftO!8z?m^)Xu}|*8(Xw;K_%4sihbu-rjI-8A$obpb@+1cRdc*Qu|mv! z;h4T&Gs~sXmo^cvqx7CKDhyB8l;fuZk34>m|MjoW^PArt$LnU=gBvKVB+G>0OPpp{ z=&KH3(OBNaKyz5vLq6#iCDH!kb09<2#7S}YAls0B8VC01D(ln?!Idq zfA*(0as950wgkiq@G&aIOTfhAl8K&}4;KG2^>yl;Aqoz~ydna5tD-#!$IoQ!+jor4 zRG`%wAk+fRQtLB|gnCA~RCnHdHv&?K`+BFD_tX5pe8{Yy1vqo~X6Eof001BWNkl4=8e73^ProVh008Nf$u?l^s+Yw3-kt?83$PAi@6XngG(v5e<{EB;!B^+HJI9* z(Gd&z8tYB*xiN9*UwBDl@c3!-O6t6H6I293ENi9WpctZ4!MDEuB2PW{Hh=s__w%_2 zM!4<@N0DV%Xgn`U9iNPe5eW5Fl6rF>0G{a}`TE5$ZQQNhQ@!@FX6$RO3DE;L$EJ$; zEZ!$nAf0&aLk{HikZM%c;nbAz{2Pw%J^UJvJ@yPIP8zND77EwGd>})>2UNhU1a0kp(;HMumqE~%|6;Bt!N|#BQi}@b!rEN#b6Z?a*bngGR_n8BC5op(Ilrg zXE^CCFP!FV9nJXaXRqYY{-Zqd_yMZcaQ$kcE6dA_kHGjmM_w8 zJ&vE}V>vdyXJ7B7P-9=H{jn6szgX5onP}yJ=U7K=#AHA zCF?b=Le^F&QsdD}^fTQ#`aRWs##a51Sz|~GSeSmFC@vo)00$10{PVZ=a%kUXzV?Ni zxNc9z&;aDFONQ1ZR|Je0I$wvr8F2!pF8L<(HAg()y6fC!;kuj7Sc(cTc4MUJG*@ z-e;}alBXlFc3Ap3G;X*W5+h3LKtZzt9|EW$E>M*o=M86Pxc>v!aNCVTj1DMDT~$FO z8tA>xOeE$>FHmasx}yJAB^S7Q`w*Y~)Q$XZ?*Wcaj-%N?T^qg8iE{PI%~)({yKJT&)GDj_!OHP_Ktcip0QbViIy?IlGlnO!T40bwo= zUbBSnPeo`_Cd6T;1U$Hm(mW^5RJ`+cmw*4(Yy9*V`#Cr^hO`O>2RC5BVO|jJjo6p` zdnFQUh4~p1Ql}>wBAt4eZUHUO3LhqLQXyoN-AV4ac{^YI@||qo620xx^LQ$kz$Qv0 zdWgYDJLBH>Z|0*P+0A!1O>~rpvki1JUm3mvSDZBv zh^4h+B0-pTZ#QWlbx9QwrN|YhZG1k!>+g1X=igsv|Dh>9^NHPj_?}H{8O|8UtrngG zdsKheRBq6}`qAKS+>zk8QXSx^jaLUX}aLgor^al>b|hGQiXs~s`Lqyi}kD&PkQ)*=K2H-#}p z@LgJ2htE8C7uR1iL`#KCDx&g;qkj$5#8RaY_ry};47Q9GeD0x}dGWO~yz<64niphw z)Pa)FXelHTiS>rf>ew;Wd=3w~Syf?Gg|+%(Y-fi>Co4U1xiZT{n-P`*!8h&}g$!KQ zrt+Kk@e^K`N6v{9J)p1;7Lufe~r@P;U>xsnW%lvX!ji}|(cpM#dRuSGnafGTwDY^TuEBW+;JJ_}*Ba?tyUCBdcd(WG= zrql-$ON#(42RC20kxzf(R`%`x4cUm4Klx zPz(%n;#8OK{oqYr+KDNRAQ>i(N*z@Y#>PEwAL{bs$ByvmPhREicLSX;OtB%S)G54G6qcis zfhZw_+Ivply*Ax|`S!K7-Y1u%{pXu^7IOU6c&=xExAT41`3bL`n{$b_&Q~C*zxMM| ztpP;h_$wg?bC?8>4q^c@N9a~;9)(YT@&>kTYSGdt^eBy(XRlI1^P}~NcP0``Mchns zB5WSb`S=6dc=$2p)%|0b3{kWMtcyqirkHi=T=ZF=GOG{Frys4C_3K*VVnj+8axE8n zoY}`(=y8_f_zP*%TDDcr|DBro#xP%17OJ@nbYGfPWzQHeYGdQ_=|BtRavEGNcc!~h z(nMFkHu~V%=73VWeiidycXc} zpcPbs(5=vFia+?&{e1L8I~ZykDiysAP@4{KpS8uX#O+KZ0>L^6Em{iK?rQPSCvW8N zH(q4Y2QWd@6TC-p*gWQdwZ6HSnC4=P^W|oxkH1zpx4L~G3Pd-rt4cFpHqZ8lxR(~H zvTr|}=_x9$4ilFg;5c3LQ{GY=;;G=y`vIgq7!)U#3XxP zIL*V4?B%(=2N|Cn!17TfFTfc=I#>{d3?<{Ny7vu@Cc*SCiNun#oC~aFJC+eWjaCRO9Bm1%``^kKK0-zkcd% zo_g*C$S8T95zK@7bPFbyoYiPxuf{el1?P-FfH>_K5UP?;oo%4gSnhMK3D@TdOC%OX zW1u%FAjVwAm4yIK5pe_pW`?>BWT3gyZf{|1GVrUXPVvItpK|NXTe$z88~Et`S8>f1 zEk*}~A_p}=G-IK4trJU+VVHrc!-5jbFcGFo;mjnw{a(qFPaoi?zu3zwFP~y;Tq)Y4 zNIOTJ!<&yXQ|8cS^V90O_j6ueImge5MB@BpaU)GiVkt|(ykOol)UNpaXW!55Hw`kB zm$mR)EMIAjwusFf<70&eN!>89w1`kihQ?Yhv{cx$YlzQ$@>cf#{$ae|0ND_xU4nE- zH((;MzOa5jIAGR9vPb%J&`S$tIb-KI+2x5u#G2g1rXF>_C*7j@y4CsUw5ZHLD}B%VH@f z^R@0kI7fDCzs5m-EuEXH_ zdY3c?dac}UN&6$Q^aO+;8G=VOAYK?9$hh~;ZQOL-CZ2n#i*qd`>jK^SzD{$0BC%ev zP?k2A2i>gd2J3cBd@So=AYy59N+kN}RXAw!&-)ChW??5eMj(Vx3lxE(NXQ6v`m$>L zp%`e9dieLQZFnjmD!?jlp^1*xdaPPglxpwymn>S{R4g^#>bt4>tBLtb5*HVW7 zDM8l0MK}L7IrGykoL$plF0YWy`nMlF>vh)m`*iS)ETAOOT*sN~z5QOT{@2WYZM})o zyEd~wrQaZy_>Fq`KeZv=moH>yG5_2tQ$VLFMB<9CLMSJEe=r*6@p6|U&b)54f zNN{od%?yu64ML}4$7q|ce*QN0TwUPsAW;Ki=DTPA6gOQ{*~7$ABh*oH(&(B9d8S-_ zMZw2Dcq98>-$(FDsUBR3BVWM0npmq`XdJy1$GZ^AJ^n4vT(P{Ib)IWmVtQjZ5UlE& z`EK)2&p;2MHqf;~eBlGba!!R5?CR2a{t0(>dCsAEdNWVRtSyE1tb z?jHM>qF}8)>#D3p#jMVe42VtQR0vsyYj0!Rd!BoJj5iLy#?OBHHut>$I_|!67kA$} zz}8_HXl2phms)kW`7WZU3Eqd8E8XZkGA(C!makUetU`&-v8>)KnPsT->1y<<8e4x`Lobv)nBu*ms`vooorh?%Zk^72bnUMV z6RQvf3ZR*#gMttkX=mK~zH4}5=X<<-;B<7HtKd?VwabR(oS#;s@w*z^)X&AtzjJ-3 z(74jX0FPPOwNRaHpj)WASk)Dg-l|@DOYr_uPl?3&nQI4dtuKfp^ZgFz6Ze{FI9jL> zai}^p&mb$vvYc);$-aFj*}wmn{OCu*`)=CJ9XH>=-FNTgs;dh&ZWP+Bio$hcUGCs8 z+9(d~J4Celtl)IQGTneJ=8^!wwdUvKs2&3l*MAj;LxhyS0w_kObExbA#E zC$s9_+rE4JU<}4!fB`TtBnXHAjYv@>D2bFP%auZ0tu|gf6Yqx?@&21lUORp!|rGV7Y|+dbXWzB@l;W4iAxUzJ&T@|^QMf?|hW*_C^< zsG5~$RvowYT^wKis;F_OI7%yca$1EjH`nCW?Iz#<;dOrevpf9s7awusP7AUrB%8$6 z>KGBMDS;N=7ePEuGS(*brHRCrBZ`^=v_!C2SEt>)%gZmGB{OgZ@&bfcO z&b4Qc@yg4mdFlzn(W#u#nlO@CR2A>q$NbNLnuuz<| z!3ajjSek2ba#HxcuU+KQ1w)<{_%cT=>CjNv+-pryXn5V?Z^i^oPMyEnFsS>LwG3T|x0uqSTN!jM$RjeC-^c|LhSa zMp}p(d}zS(xz_Nu`tG?03w;qTNpB-Y766nWTs1l0^t2@xvdVoI{1bS0T> z;Jn~5P*boivOEJb!(6k*gSQ;-yz?H%j(orqmnOM%d4lJkKg*LB8%&R9)N(_uX2^_C zWv)ojj*UOWhHhCo`PLV}u7t)fD-HSZk1We&*L8Lly3FWbo;R-D0}#Nh2%eYTYJGln zkzjgjb;j}3s&Iejp)8eFr{vy}=fmp-KmXZn-hA^uZ@)9gjavn+BB$gSSv^B6SOZ>5 z6bCj;rZ{y71}hmtJ){|UP*Z}G>p1_tL29dsgM)zP;4_NFd9FQK=Nn(U#OWg$88t+6 ztObw9kdut_#O@IdbhY7CNcFmNSX(kZopJ5ill`+sRM{r|nm*%PBYedR3Y&mZIJm1CSbSz~fM zW3(YOYD&#IL_2r|(NP2&ItZ2C7(g_tn#c1rK`65FS}VaJjrM}crR!85^kDa{*RYG> zdP{0gcCIoSLw^*b3SypKPbXt}*uBO+t3{}RoZ&R1<1B@+=1t#a2E4yG5nYznVy7_nF%V=d>SF6SnL6_HS1ew5;Qc4o9Y%{N=Yl%X3$! zsTq&a(a`6bUIfj79^54M0or|)3E_QfFnBF#)G}Uq=`8>4jgR>ChaDuVQ+h|9=Xmc| zR2+f**)Jsa2m9f@xSjO3t*v^ON#Dd=p9g(N4!U~hU29QLPd6@B`qDL1!%rl(7KzI6 zy@nn_v3(pjI+PL>i}4ogCaAp3DhinkI%itP)Q;nva(B^l_pO3o{OUU6qc=H!W`;AT zMtSmy6I?n!&f{k-$7U?GJSWeDyk;;4Of-6Xku!?=5^I7-S`oaOSOQ+VhK@vK`${4% zL})c<0@_+_?3CyAKxWh@dit%_cE58z)E>G2#${CtUET=6JF=iQr;tfDgZm z0wtItdY4PpbeDTYtdFh2QCPz(7L5V$^nvg0Z4oNxa ziCtq^eV? zAT_LIb3s(Sf>ETa-5$I~Y22AeY$>9BUgM~Mu7+NycNj#5?@auTSn@FDt_gUrco9sV zBbi`oV`%Ak(CqNjx9{@9pZ=PEctaSgD<_VPa{9y+XHFmI{Q0Ate`16qM{~xyvdz=OT2%5miZ;2*|B(Okkv*p*<=`` z8;>y_V>*bGVbVauK;HNasDaiH@;Fft>mo~u;$swm#;|q?(G4p~@Ffz7t;Sl5bFTZl zcaCD|0bl<76~6t)Px9FD4DlsmLwQOcT@|8=s3+)jBm+IM8_2*v-FLp`2o@`zBa;~~ zJa?ME`P(0|RMyGsBY5vI#v}thk=VybFtmHhO6t8Ymlpb$88pycb58LtxTp@^OKZQY z5FW(iKg=jqNuXF@MMEufq8HXjPkBi3c6i&4VcpSCSHYK_%v66DVGybwQ?q0>gZc(p zy-vrK6dkzxe!uu7!SSQQxE#7-t%>_=(H2KEsmwDpc z1a(`6=pF@?3@?JHVmu)=CC#HH_5>a%2@j+NFNpPwj=utIgtu>$WHD+jjit7R z{XVANPo!%-oEVe}c<0Ndx&H4440P|BbLze8CZ7UT>8C^@vBgN0*G(|^5WogwSHV<9 zI)HebDMF3&7>h13dprcVSnwfN-5ZZ;hpL>K&1*OlXX$Enz|dBuRVcF`c;5PT8}-+z z*$)`4!_>It_>l(FlXYe$N0^x!=lHQYGtv z2S>VeBiNTm^m!4Sg3}NM;GD-hQ1z6Br!1ASQ0C@3+@CGD`@k_fyU6W3v)r3qVs5eI z)+YhE80LJvLt1m%)FvzX3h{SMtjT!cFwm~J7zEcxx z&3A(aqB_3XV9>`9{J!k`l_$F@%Z>VK-L4qGz_NW0&yeq{Rfp|gR{4GX$NBJOg~9We z-cP??z{ao_M-cz(&Oi$JMlRv!1E1#KSqG1tA zgfFNBPx_JE&J|p5c6q z+NvhC(=(h%>_d9vB~|Zy*<8awFI#hkFI=8$7{H1lIN-y}Om!mz&-Jm|49a?yg*P%Q zvys`F%_S}I+yeCW@*W}xZM{nzgGul}w=_lb)45N*uvV5sU;wdeVdvH7}A16|QL2~fok zxcIyD(j7DkJka$5hWSxV^KOlK~2H63T*L!FTC^= zU;DyYj*M9{A-0V=Vr+a;L%I+`P{mG$O=8!mqT#AuJ!q?_fR|9ZQ@k)<7oNO4${XKr zQIz9|&G9H=+KA8MdqPp;@Dd{NmOs|Z+ulaUTAS^Yc5?i@*<;=8Ha9y?-~9UPUVXT5 z?*`ay`(Y>T$IY^GUy9=JqBsGk>Ri!U-9UF`b57OyP~W%eHZ*#Qg0a5U2Kq3DU9_E4 z4BqVi_u}y<_9BA#o&l>6^&W=YKLs-+9<$G(4)|{C6zo9fYbyg~;Ms7POkyukeQq$) zK(!9up{f)j6yBnX!YA_{_i+p7=TR*I#aJ*QMjsOe(+rBWW53pPq%S|LtM^a;-Oh0FJ@lw0Ab)nRS2GS<6)SDKkJ zSWT`M-|&dU0a|zBvp*BRXQYPw_827r(Z~*yImTL07gGyjx`7Z3^6un=;=L!T9E7dM z1)U1`zDXc4y$J|k9b@&mbSDC|o6Df6`mRxLtOP+lmjj`;uh$|>E*PSrT?S(xjpuKw zvy7<*gEvDqeVvDMnEJ`hzQ1;oq5F>!FGdV%B#J_2WRgMAfnt^ypPk}A{KZwSTo@%c z;fV?3U%jiNwKUNM{y*L`*P@qERN+V6DN%L9gQR-eHV}&L)bh868J_1Me%f_c#=( zyy;a3Y6`M!0Zb7*s78WOkHMJeP4IvO!_w2&$x1Nhf>@60O#*Dm3ulKSH4w!2yvEhB z2RovgP|z4xA^6JMV&WYVj~5pWbRPzG2$oTNS;za}9k1%bqoItSs>jFDb>8IoDzv}y zs+cHL*moI>8m!dgb3vT{2>Uc9+`FnYpXw-HGgLZ=w6S@Kkq%yq?gRvCKy#puYRu)Y z#1Fg1dvyRBeV<8j`>^6;X+tp~LPQm;AA?SVqBFiHO$YLXQg=qt_Y8%-nsPFUa zgO;93jR0?OUU7vFLRex@N}O+_zKxNBye2dnmMk||3zC&&dDd0z_dZOzRD(lRr6>xD zRz}e=bUJ15#*5$;tg$#073mu4-gZ#i!5EL=a9SYVW5mWVc8l>Do(we>FB-$w9i|HN z*ZAbb(D#t%pZdDPiVCr9v`{HA)G*#qwwfHDfxrBV&oSDu@c;lI07*naRP*J}A7eBx z$&B=(UJ{9Y#`3f<^^S@08t2cQz{*XG`k1;#H`=TBQmDN0X|SG1>;~%|Q^qKDkuc6d zz4PV5gZ@R9dC(VUi3WP07bY5pONm6{pi|XZ6_K7n46!UxTqqjO-p3Gi0gawuj|+M3 z5{+}|1t?cr3(?nAed1ndv#Rv+>icrS)rY8_e#5Z}->$^yddI%c;#Vc)WUDDqaiR7G zf~p0w_;0xWsu~i#RihfUqOQc34)1~zaJ^PCF_tkkHNxb0o#~kg#>b8_(hw%b8%$4+ zF*Y{BNW)Uk9kpD@@_fK+u1>g+euI7YR}K|&sRI9{r>!|cJAHU>ky6UxI9(u@@@e0fCoIJD~A;{s=8}b zp;upRp}Qjx2?n{WbszeVRlA?>hil(^ZX8}*6z&Uo*(QqDRXuJ9QwIvgc#rxLUv@B* zSX(kNUT1o;!LcJVoIZ7wqemw=dCGF?RE<+7N0=Bl)N_M1f;B=WF^xci@K!FBR@Soi z77i2uuS(%PZAzS1yce8@R!4d8K)L@w`Q(!hAANL(n>X)r{pK8>+--9I{vu0@i>Qp@ z%_u5C+{k1Gqzv`vh2RR(ASR#+>IFlF_zd+KqFIQ_*rAeGyZsF9cK^52^q<>uor>Od z5Fi6zL(2@`>QJ|{{L#0r@yCB~nNvsVP!`<#bcQRjp`1*S<*latm|FHMRM%Eej-OfEfy^)t{S*%%R zO?}bUa&b=TeHmODLoQKd8yfvaPJFwT-KlNv)G=*Dv2d>r{C4}e)^n}a>PUC<`P-Jk zX!o7p^M&^Y)71i9$9lKVs6LeLm%Y4VRf)oPz2g<>yRNc)px%F8EAK^bx$R-SZS7(A zsi$UN;!Ov!7JPBk0W#*m(-0V!GQI`!@S}!u20A4aCA8a9 zEG?bq!FTfOw=PTC)j^8V>xXZ`!%TtB%N#_5lu&cD^q&!Zw5-c^F)%f&hMMZ5p| z0RtU%PK$-vFpie4_dT({*zHD>*j={g{#O?IK#cn;2M}e}*ToQ{YiJ$sy~##j+5;~H z0gS^rkI@>&k3pG1=~0&OI?wd9a^$f(S1z36%9&$ayl{dGkJmXeW2w~)nYA=(f;A2! zB}NKFGDK~dQ0?l8#DSX*?%x|9OZ%;k5JcjP8|(WdOxNZn3?8)xj9UD(L8;TZJWA=O zXm%dw?gPh1H(LDiSF^nJt9$(FomoD!U*6=cH}7!!Zj*Lfu#E{!*1&n&y znIoJ!YG~w^MlHu=V5|di;K7KGwV{dL@|Y^1ipFwk{h_{H9TFJ0v_|c^u5>p=p}Ozg zD2E^=Eb-rNT0$(1XVoAEvYIfKTbRyx{A8V1o*83d@e*&nTk?}%-sXotevdcbxy#)L zbCfy?rbcFJSd$~-2J{UcmF=j4kt%C!52}jyK7{%z_zrZI=6UV2kMZyS{288mc8bwV zu^NT4L~(RbL(nf+cLYx&vG)-iwajqp#8E~^KA@>VD^zTy|3Z968+b@W)P7~V)7W+* zO>VnL9`t=o99R2d;hw5n-Gh$$(w9qjLwIU$;5=B9Hjy}tEPwtFTYIPs=V9l)azG&K zeqr>ls{}8-_WfRcQ+-zDMeDNlc7SLI1bp9Ks}SwqL>)B2B4}Aq7ESOS zj!um7#M7sE=`)Y>+_h=Woyj;pWf-k{>Y|_yi$&Go$>QXPA{q>RgMg1$rfWrKU0nd} zJH+L^c<{ile=0=9_i}r++D}x`?=A$0M=3xo27_oge;dEgMHm^iOpazu&InhY9_4eN zKgo}OdY}LPgAe%WFFxVM&HI$TfvHVkY#mhxCf0H9zvkfzgioT7p`!E^Yo~j3O5eo7 zI@2{Ci8Q6XUV3*Ts#a?d1q3iCMwFsx;W|y8yE?;n{``49{lYjSHHSFci|UBEd|sOv zsnd)0NhA&eLS}_ir^gu|%UFEiz{GL7NQ|9=y)iD?nf`CBeDU>f16FQgKlR_*cl^EI z*FVRwpU(B55l6LgOyfEWcTvBl2fcKi);-l6C=o*a+HeEio@n}8i*!|!``L)_z+R-k zJXS?Gr-M1Ox-qJfV2^fxm#?-RkI6=aD~2t*TIamp`!8ZvY$KRHj@|%Zt4@6R!S-+0 zv69NzT5JF7KQ=Su<)lNm9}P`WR9vrKInwoD3mKw<^=*vx6k4K2@FElnT6*X-8O=Ie zxp0)1oA;TCreO8z{;`#VK2z49S}-EP0D zLtv`M`3zUI?&(?vI_gW;nZJV?28Egq>MV<{(m?mo2x}D%!Z~aYy=Qs1%PH+$hPR8o zGdsWCyBIPNf30{8wBCox2 zg(okLb9~A$Hqvh-YVgEMw)_BA^uPlZs2;+qi)_o|zevu`ywMBFFnSHion7z;oXpAvBD`F#g#K%LN7me@ZWc1L2EYZmanYkT*9pDka( zmrHZ%mKMe2lRI^gyU9U(Kn~(LC3cSV{0t)+EO&_Yn>@a_RGK6jeWzxpD-^~KYiIi54w5HfXGV?js2E8aP5 zZr0f6!N*G!nN#f==s{7h#8_h1xN>wnV`{XX*;(4{zM!jeoq!4}W}{h1nXW zRwo-3ls0%ls7H}LAI0s$ru(y#Mf+^V#1^3plup!P`QV|=dBBMQBZwNLWy#8t@zHs{ z{YTgMgV&$niO0sNd+>$FHcVF-s{h|a;=mJMQ`QJ$W1|QfqDi!>ppgPz6Z?-;1tq4b zxWxr6mll?PyTU-%a%tX|i}NtS!Of9K95gnf=-Kb794=C^_I9>HJ#;NLvhCzWT0g_C zmD#|{z-ti7w34H0qs&zazJn_k@SP^7Pfqih7a!-Fzxy1Q&X00pCTGMHaUfL#XR8!w ztj(k0csM=hisEnt!8nQ2z$S>%X$gcH?*&%At+|NH;>72o^k z+r0J8T^ys38FE|4c?o6TjiOd@zQjm1&UI<7IkG+d$8t%+Slz%FysuIZ6ek{MluR1f z!cffJDv#NlD6;X-Ihqm3G>E~HgeS`Xcl zrR(4JUTr_a`=s?B>*ho*w~yuLwO6nIM%e6FbtBi{QO4?f9g(1f7 z-ds?(xG-p&@BarWaWL6UFG}*@93GSoCIgWg-U;ICSiqHws9R)eqTtmpUFCOQzsA)| z4USLcj*aT*NLd@Y<(O@$bL=BA>c8#>{ly)$=QZlfBDf)M`i^9)=n+ z8v3rx2qTR;*1$mdV;NM_NG&6g*!%dHedo)i1z$EF?m_qE;+!v+=6bn%;U%lFNKGPf zkl61ta2p|o6Kw)cietfieiwNk$sZZZ)#vcVi5y9_I>d79txuohC)Q z&4^v%H@|$2KmYxw`OLFZjAtffkz}C8Q>Z5si(zH?z(nG35Z)G9W*8r|NhMn__v?D$9!uJS5A>Nrm&`l*RZl+ zFh--OTw?2}yyg({>%vb}^Vf(_mMuC(6I_R<&K~EhU%AS+zImC)j*L*#-aYoJs0voX z#EOuUCQ}lJ5{U-7HH?l}>ba%Uam!jmh($`%Sc$}bAe87;5!5&5{kn3{ebIKs++Ear z1e1!;Bn~pktDJZ^g1Q=(35w2rocn}luFdeZ-+G3xed#>MCJp0tCG$ln5rsod2^iE^ zyc%4YQOonLscEDjcs^=ySZg80OR`qkRw@7{aFC?cL_ zbDnx#c;>0&{Kr3inNMAt;P~-6B5hEmqZVsw;K2&yIMjPE`R<)lBC)Fog2#wZuM2r@ z(RO8!LmH(g4mt;MtR7mh_r93BtG;M0|9*vm4&d6ex0Rwmq>h?u@~+<_zr8%_e){g! zdRN`H+q;ot?>~sO*4vJQ-RJ#!71h-}5ncYUu5%j^?QUbZ@-PwwS+sxOn_wgNbW{7% zzH9R+JKwuXRbu6>`eu_eCzwQKz8pFG2b zGha1`c+!*I)yrD!zHtE7dP&Y*%(4Q1ojbqOBrUAI*zzV}Bp*&wFRKCkYF^yaZ* z2)m$o=gCZtBJny2UX)Tx5HNX-vR&euOHAY?pZ)Y%UjO0+zVO+T96y$k<@6e?W`GU* zuV!1YAdTKJBoc>!mE+3T=EP%jkFf=4Js9Y=9dpt1o_Vv2`MTRTrrF)W>S}xMzgHXW z$A_i4ZRfjSy|%_MwsWjDcr5E5zzr8v+`#^K`1&u~5n=(=!nGdU9Ja`C4|?bRP4COn z+Ax5ujF^?af?cC<(DQHA2ZZ# z2ZKkn4r=3spu~W~6_)1^*0aoEWIfkpZ8t#hxPqu+4V*b%vLT zdE=jc&ifzU#CK*;Q$sREa>3+^YKf{xt)aw&7jQ(cxOCOU2g~DC_g)e8Lyv3Eqj9)W z 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 literal 0 HcmV?d00001 diff --git a/setup/nukestudio/hiero_plugin_path/Templates/houdini.png b/setup/nukestudio/hiero_plugin_path/Templates/houdini.png new file mode 100644 index 0000000000000000000000000000000000000000..128eac262af7069b9c6dd226b570c5599c9e325e GIT binary patch literal 11586 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 xvm1w#tEIVV-5fy!X*eGB_{kh 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 literal 0 HcmV?d00001 diff --git a/setup/nukestudio/hiero_plugin_path/Templates/nuke.png b/setup/nukestudio/hiero_plugin_path/Templates/nuke.png new file mode 100644 index 0000000000000000000000000000000000000000..9d9dc4104c9b2b8a27dd8b17846f54e10b857584 GIT binary patch literal 65305 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 O`N&8ph}Vi52L2zL*R3M} literal 0 HcmV?d00001 diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox deleted file mode 100644 index 684cd0d1a2..0000000000 --- a/setup/nukestudio/hiero_plugin_path/Templates/vfx_aces.hrox +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - 2 - 70 - - - 2 - 70 - - - 2 - 70 - 13 - - - 2 - 70 - 17 - - - 2 - 70 - 2 - - - - - - - - - diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox deleted file mode 100644 index e915a24084..0000000000 --- a/setup/nukestudio/hiero_plugin_path/Templates/vfx_linear.hrox +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - 2 - 70 - - - 2 - 70 - - - 2 - 70 - 13 - - - 2 - 70 - 17 - - - 2 - 70 - 2 - - - - - - - - - diff --git a/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox b/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox deleted file mode 100644 index 42659cf81b..0000000000 --- a/setup/nukestudio/hiero_plugin_path/Templates/vfx_rec709.hrox +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - 2 - 70 - - - 2 - 70 - - - 2 - 70 - 13 - - - 2 - 70 - 17 - - - 2 - 70 - 2 - - - - - - - - - diff --git a/setup/nukestudio/hiero_plugin_path/Templates/volume.png b/setup/nukestudio/hiero_plugin_path/Templates/volume.png new file mode 100644 index 0000000000000000000000000000000000000000..47119dc98b5935b2df0168541708a0336cf2cbcd GIT binary patch literal 19442 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 literal 0 HcmV?d00001 From b586383a0cc1c268f06455d43197671c4480a51c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 11 May 2019 18:19:47 +0200 Subject: [PATCH 158/193] fix(nukestudio): fixing to new hiero api --- .../Python/StartupUI/nukeStyleKeyboardShortcuts.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py index 36a30e3a3c..41c192ab15 100644 --- a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/nukeStyleKeyboardShortcuts.py @@ -12,25 +12,22 @@ except: from PySide2.QtCore import * #---------------------------------------------- -a = hiero.ui.findMenuAction('Import Clips...') +a = hiero.ui.findMenuAction('Import File(s)...') # Note: You probably best to make this 'Ctrl+R' - currently conflicts with 'Red' in the Viewer! a.setShortcut(QKeySequence('R')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Import Folder...') +a = hiero.ui.findMenuAction('Import Folder(s)...') a.setShortcut(QKeySequence('Shift+R')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Import EDL/XML...') +a = hiero.ui.findMenuAction('Import EDL/XML/AAF...') a.setShortcut(QKeySequence('Ctrl+Shift+O')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Show Metadata') +a = hiero.ui.findMenuAction('Metadata View') a.setShortcut(QKeySequence('I')) #---------------------------------------------- a = hiero.ui.findMenuAction('Edit Settings') a.setShortcut(QKeySequence('S')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Monitor Controls') +a = hiero.ui.findMenuAction('Monitor Output') a.setShortcut(QKeySequence('Ctrl+U')) #---------------------------------------------- -a = hiero.ui.findMenuAction('New Viewer') -a.setShortcut(QKeySequence('Ctrl+I')) -#---------------------------------------------- From 2cf60c1fbb4ac249597010d9153f9612ac5dd052 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 11 May 2019 18:20:14 +0200 Subject: [PATCH 159/193] feat(nukestudio): work on plugins --- .../publish/collect_assumed_destination.py | 2 +- pype/plugins/nukestudio/_unused/collect.py | 191 ++++++++++++++++++ .../collect_submission.py | 0 .../{publish => _unused}/extract_tasks.py | 3 +- pype/plugins/nukestudio/publish/collect.py | 183 +++-------------- .../plugins/nukestudio/publish/collectTags.py | 14 ++ .../nukestudio/publish/extract_review.py | 3 +- .../nukestudio/publish/validate_names.py | 5 +- .../publish/validate_projectroot.py | 3 +- .../publish/validate_resolved_paths.py | 4 +- .../nukestudio/publish/validate_task.py | 5 +- .../nukestudio/publish/validate_track_item.py | 10 +- .../nukestudio/publish/validate_viewer_lut.py | 3 +- 13 files changed, 245 insertions(+), 181 deletions(-) create mode 100644 pype/plugins/nukestudio/_unused/collect.py rename pype/plugins/nukestudio/{publish => _unused}/collect_submission.py (100%) rename pype/plugins/nukestudio/{publish => _unused}/extract_tasks.py (97%) create mode 100644 pype/plugins/nukestudio/publish/collectTags.py diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index fa6a3d9423..4f9d180843 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -9,7 +9,7 @@ class CollectAssumedDestination(pyblish.api.ContextPlugin): label = "Collect Assumed Destination" order = pyblish.api.CollectorOrder + 0.498 - exclude_families = ["clip"] + exclude_families = ["clip", "trackItem"] def process(self, context): for instance in context: diff --git a/pype/plugins/nukestudio/_unused/collect.py b/pype/plugins/nukestudio/_unused/collect.py new file mode 100644 index 0000000000..4e20202fe0 --- /dev/null +++ b/pype/plugins/nukestudio/_unused/collect.py @@ -0,0 +1,191 @@ +from pyblish import api + +class CollectFramerate(api.ContextPlugin): + """Collect framerate from selected sequence.""" + + order = api.CollectorOrder + label = "Collect Framerate" + hosts = ["nukestudio"] + + def process(self, context): + for item in context.data.get("selection", []): + context.data["framerate"] = item.sequence().framerate().toFloat() + return + + +class CollectTrackItems(api.ContextPlugin): + """Collect all tasks from submission.""" + + order = api.CollectorOrder + label = "Collect Track Items" + hosts = ["nukestudio"] + + def process(self, context): + import os + + submission = context.data.get("submission", None) + data = {} + + # Set handles + handles = 0 + if submission: + for task in submission.getLeafTasks(): + + if task._cutHandles: + handles = task._cutHandles + self.log.info("__ handles: '{}'".format(handles)) + + # Skip audio track items + media_type = "core.Hiero.Python.TrackItem.MediaType.kAudio" + if str(task._item.mediaType()) == media_type: + continue + + item = task._item + if item.name() not in data: + data[item.name()] = {"item": item, "tasks": [task]} + else: + data[item.name()]["tasks"].append(task) + + data[item.name()]["startFrame"] = task.outputRange()[0] + data[item.name()]["endFrame"] = task.outputRange()[1] + else: + for item in context.data.get("selection", []): + # Skip audio track items + # Try/Except is to handle items types, like EffectTrackItem + try: + media_type = "core.Hiero.Python.TrackItem.MediaType.kVideo" + if str(item.mediaType()) != media_type: + continue + except: + continue + + data[item.name()] = { + "item": item, + "tasks": [], + "startFrame": item.timelineIn(), + "endFrame": item.timelineOut() + } + + for key, value in data.items(): + + context.create_instance( + name=key, + subset="trackItem", + asset=value["item"].name(), + item=value["item"], + family="trackItem", + tasks=value["tasks"], + startFrame=value["startFrame"] + handles, + endFrame=value["endFrame"] - handles, + handles=handles + ) + context.create_instance( + name=key + "_review", + subset="reviewItem", + asset=value["item"].name(), + item=value["item"], + family="trackItem_review", + families=["output"], + handles=handles, + output_path=os.path.abspath( + os.path.join( + context.data["activeProject"].path(), + "..", + "workspace", + key + ".mov" + ) + ) + ) + + +class CollectTasks(api.ContextPlugin): + """Collect all tasks from submission.""" + + order = api.CollectorOrder + 0.01 + label = "Collect Tasks" + hosts = ["nukestudio"] + + def process(self, context): + import os + import re + + import hiero.exporters as he + import clique + + for parent in context: + if "trackItem" != parent.data["family"]: + continue + + for task in parent.data["tasks"]: + asset_type = None + + hiero_cls = he.FnSymLinkExporter.SymLinkExporter + if isinstance(task, hiero_cls): + asset_type = "img" + movie_formats = [".mov", ".R3D"] + ext = os.path.splitext(task.resolvedExportPath())[1] + if ext in movie_formats: + asset_type = "mov" + + hiero_cls = he.FnTranscodeExporter.TranscodeExporter + if isinstance(task, hiero_cls): + asset_type = "img" + if task.resolvedExportPath().endswith(".mov"): + asset_type = "mov" + + hiero_cls = he.FnNukeShotExporter.NukeShotExporter + if isinstance(task, hiero_cls): + asset_type = "scene" + + hiero_cls = he.FnAudioExportTask.AudioExportTask + if isinstance(task, hiero_cls): + asset_type = "audio" + + # Skip all non supported export types + if not asset_type: + continue + + resolved_path = task.resolvedExportPath() + + # Formatting the basename to not include frame padding or + # extension. + name = os.path.splitext(os.path.basename(resolved_path))[0] + name = name.replace(".", "") + name = name.replace("#", "") + name = re.sub(r"%.*d", "", name) + instance = context.create_instance(name=name, parent=parent) + + instance.data["task"] = task + instance.data["item"] = parent.data["item"] + + instance.data["family"] = "trackItem.task" + instance.data["families"] = [asset_type, "local", "task"] + + label = "{1}/{0} - {2} - local".format( + name, parent, asset_type + ) + instance.data["label"] = label + + instance.data["handles"] = parent.data["handles"] + + # Add collection or output + if asset_type == "img": + collection = None + + if "#" in resolved_path: + head = resolved_path.split("#")[0] + padding = resolved_path.count("#") + tail = resolved_path.split("#")[-1] + + collection = clique.Collection( + head=head, padding=padding, tail=tail + ) + + if "%" in resolved_path: + collection = clique.parse( + resolved_path, pattern="{head}{padding}{tail}" + ) + + instance.data["collection"] = collection + else: + instance.data["output_path"] = resolved_path diff --git a/pype/plugins/nukestudio/publish/collect_submission.py b/pype/plugins/nukestudio/_unused/collect_submission.py similarity index 100% rename from pype/plugins/nukestudio/publish/collect_submission.py rename to pype/plugins/nukestudio/_unused/collect_submission.py diff --git a/pype/plugins/nukestudio/publish/extract_tasks.py b/pype/plugins/nukestudio/_unused/extract_tasks.py similarity index 97% rename from pype/plugins/nukestudio/publish/extract_tasks.py rename to pype/plugins/nukestudio/_unused/extract_tasks.py index c841b604f1..29c1350cc9 100644 --- a/pype/plugins/nukestudio/publish/extract_tasks.py +++ b/pype/plugins/nukestudio/_unused/extract_tasks.py @@ -1,11 +1,10 @@ from pyblish import api -from pyblish_bumpybox import inventory class ExtractTasks(api.InstancePlugin): """Extract tasks.""" - order = inventory.get_order(__file__, "ExtractTasks") + order = api.ExtractorOrder label = "Tasks" hosts = ["nukestudio"] families = ["trackItem.task"] diff --git a/pype/plugins/nukestudio/publish/collect.py b/pype/plugins/nukestudio/publish/collect.py index c2eeb25235..de6b2b3fca 100644 --- a/pype/plugins/nukestudio/publish/collect.py +++ b/pype/plugins/nukestudio/publish/collect.py @@ -1,12 +1,10 @@ from pyblish import api -from pyblish_bumpybox import inventory - class CollectFramerate(api.ContextPlugin): """Collect framerate from selected sequence.""" - order = inventory.get_order(__file__, "CollectFramerate") - label = "Framerate" + order = api.CollectorOrder + label = "Collect Framerate" hosts = ["nukestudio"] def process(self, context): @@ -16,173 +14,44 @@ class CollectFramerate(api.ContextPlugin): class CollectTrackItems(api.ContextPlugin): - """Collect all tasks from submission.""" + """Collect all Track items selection.""" - order = inventory.get_order(__file__, "CollectTrackItems") - label = "Track Items" + order = api.CollectorOrder + label = "Collect Track Items" hosts = ["nukestudio"] def process(self, context): import os - submission = context.data.get("submission", None) data = {} - - # Set handles - handles = 0 - if submission: - for task in submission.getLeafTasks(): - - if task._cutHandles: - handles = task._cutHandles - - # Skip audio track items - media_type = "core.Hiero.Python.TrackItem.MediaType.kAudio" - if str(task._item.mediaType()) == media_type: + for item in context.data.get("selection", []): + self.log.info("__ item: {}".format(item)) + # Skip audio track items + # Try/Except is to handle items types, like EffectTrackItem + try: + media_type = "core.Hiero.Python.TrackItem.MediaType.kVideo" + if str(item.mediaType()) != media_type: continue + except: + continue - item = task._item - if item.name() not in data: - data[item.name()] = {"item": item, "tasks": [task]} - else: - data[item.name()]["tasks"].append(task) + data[item.name()] = { + "item": item, + "tasks": [], + "startFrame": item.timelineIn(), + "endFrame": item.timelineOut() + } - data[item.name()]["startFrame"] = task.outputRange()[0] - data[item.name()]["endFrame"] = task.outputRange()[1] - else: - for item in context.data.get("selection", []): - # Skip audio track items - # Try/Except is to handle items types, like EffectTrackItem - try: - media_type = "core.Hiero.Python.TrackItem.MediaType.kVideo" - if str(item.mediaType()) != media_type: - continue - except: - continue - - data[item.name()] = { - "item": item, - "tasks": [], - "startFrame": item.timelineIn(), - "endFrame": item.timelineOut() - } - - for key, value in data.iteritems(): + for key, value in data.items(): context.create_instance( name=key, + subset="trackItem", + asset=value["item"].name(), item=value["item"], family="trackItem", tasks=value["tasks"], - startFrame=value["startFrame"] + handles, - endFrame=value["endFrame"] - handles, - handles=handles + startFrame=value["startFrame"], + endFrame=value["endFrame"], + handles=0 ) - context.create_instance( - name=key + "_review", - item=value["item"], - family="review", - families=["output"], - handles=handles, - output_path=os.path.abspath( - os.path.join( - context.data["activeProject"].path(), - "..", - "workspace", - key + ".mov" - ) - ) - ) - - -class CollectTasks(api.ContextPlugin): - """Collect all tasks from submission.""" - - order = inventory.get_order(__file__, "CollectTasks") - label = "Tasks" - hosts = ["nukestudio"] - - def process(self, context): - import os - import re - - import hiero.exporters as he - import clique - - for parent in context: - if "trackItem" != parent.data["family"]: - continue - - for task in parent.data["tasks"]: - asset_type = None - - hiero_cls = he.FnSymLinkExporter.SymLinkExporter - if isinstance(task, hiero_cls): - asset_type = "img" - movie_formats = [".mov", ".R3D"] - ext = os.path.splitext(task.resolvedExportPath())[1] - if ext in movie_formats: - asset_type = "mov" - - hiero_cls = he.FnTranscodeExporter.TranscodeExporter - if isinstance(task, hiero_cls): - asset_type = "img" - if task.resolvedExportPath().endswith(".mov"): - asset_type = "mov" - - hiero_cls = he.FnNukeShotExporter.NukeShotExporter - if isinstance(task, hiero_cls): - asset_type = "scene" - - hiero_cls = he.FnAudioExportTask.AudioExportTask - if isinstance(task, hiero_cls): - asset_type = "audio" - - # Skip all non supported export types - if not asset_type: - continue - - resolved_path = task.resolvedExportPath() - - # Formatting the basename to not include frame padding or - # extension. - name = os.path.splitext(os.path.basename(resolved_path))[0] - name = name.replace(".", "") - name = name.replace("#", "") - name = re.sub(r"%.*d", "", name) - instance = context.create_instance(name=name, parent=parent) - - instance.data["task"] = task - instance.data["item"] = parent.data["item"] - - instance.data["family"] = "trackItem.task" - instance.data["families"] = [asset_type, "local", "task"] - - label = "{1}/{0} - {2} - local".format( - name, parent, asset_type - ) - instance.data["label"] = label - - instance.data["handles"] = parent.data["handles"] - - # Add collection or output - if asset_type == "img": - collection = None - - if "#" in resolved_path: - head = resolved_path.split("#")[0] - padding = resolved_path.count("#") - tail = resolved_path.split("#")[-1] - - collection = clique.Collection( - head=head, padding=padding, tail=tail - ) - - if "%" in resolved_path: - collection = clique.parse( - resolved_path, pattern="{head}{padding}{tail}" - ) - - instance.data["collection"] = collection - else: - instance.data["output_path"] = resolved_path diff --git a/pype/plugins/nukestudio/publish/collectTags.py b/pype/plugins/nukestudio/publish/collectTags.py new file mode 100644 index 0000000000..5137a9f22f --- /dev/null +++ b/pype/plugins/nukestudio/publish/collectTags.py @@ -0,0 +1,14 @@ +from pyblish import api + + +class CollectTrackItemTags(api.InstancePlugin): + """Collect Tags from selected track items.""" + + order = api.CollectorOrder + label = "Collect Tags" + hosts = ["nukestudio"] + + def process(self, instance): + instance.data["tags"] = instance.data["item"].tags() + self.log.info(instance.data["tags"]) + return diff --git a/pype/plugins/nukestudio/publish/extract_review.py b/pype/plugins/nukestudio/publish/extract_review.py index 2b688cb53c..537988e0ad 100644 --- a/pype/plugins/nukestudio/publish/extract_review.py +++ b/pype/plugins/nukestudio/publish/extract_review.py @@ -1,11 +1,10 @@ from pyblish import api -from pyblish_bumpybox import inventory class ExtractReview(api.InstancePlugin): """Extracts movie for review""" - order = inventory.get_order(__file__, "ExtractReview") + order = api.ExtractorOrder label = "NukeStudio Review" optional = True hosts = ["nukestudio"] diff --git a/pype/plugins/nukestudio/publish/validate_names.py b/pype/plugins/nukestudio/publish/validate_names.py index 571359a3b7..169febd764 100644 --- a/pype/plugins/nukestudio/publish/validate_names.py +++ b/pype/plugins/nukestudio/publish/validate_names.py @@ -1,5 +1,4 @@ from pyblish import api -from pyblish_bumpybox import inventory class ValidateNames(api.InstancePlugin): @@ -10,7 +9,7 @@ class ValidateNames(api.InstancePlugin): Exact matching to optimize processing. """ - order = inventory.get_order(__file__, "ValidateNames") + order = api.ValidatorOrder families = ["trackItem"] match = api.Exact label = "Names" @@ -39,5 +38,5 @@ class ValidateNamesFtrack(ValidateNames): accommodate for the ftrack family addition. """ - order = inventory.get_order(__file__, "ValidateNamesFtrack") + order = api.ValidatorOrder families = ["trackItem", "ftrack"] diff --git a/pype/plugins/nukestudio/publish/validate_projectroot.py b/pype/plugins/nukestudio/publish/validate_projectroot.py index 459b487bd2..b9b851e0d1 100644 --- a/pype/plugins/nukestudio/publish/validate_projectroot.py +++ b/pype/plugins/nukestudio/publish/validate_projectroot.py @@ -1,5 +1,4 @@ from pyblish import api -from pyblish_bumpybox import inventory class RepairProjectRoot(api.Action): @@ -32,7 +31,7 @@ class RepairProjectRoot(api.Action): class ValidateProjectRoot(api.ContextPlugin): """Validate the project root to the workspace directory.""" - order = inventory.get_order(__file__, "ValidateProjectRoot") + order = api.ValidatorOrder label = "Project Root" hosts = ["nukestudio"] actions = [RepairProjectRoot] diff --git a/pype/plugins/nukestudio/publish/validate_resolved_paths.py b/pype/plugins/nukestudio/publish/validate_resolved_paths.py index 110b8772b5..f1f0b7bbc8 100644 --- a/pype/plugins/nukestudio/publish/validate_resolved_paths.py +++ b/pype/plugins/nukestudio/publish/validate_resolved_paths.py @@ -1,11 +1,9 @@ from pyblish import api -from pyblish_bumpybox import inventory - class ValidateResolvedPaths(api.ContextPlugin): """Validate there are no overlapping resolved paths.""" - order = inventory.get_order(__file__, "ValidateResolvedPaths") + order = api.ValidatorOrder label = "Resolved Paths" hosts = ["nukestudio"] diff --git a/pype/plugins/nukestudio/publish/validate_task.py b/pype/plugins/nukestudio/publish/validate_task.py index a48ae115d8..ff8fa6b6e1 100644 --- a/pype/plugins/nukestudio/publish/validate_task.py +++ b/pype/plugins/nukestudio/publish/validate_task.py @@ -1,5 +1,4 @@ from pyblish import api -from pyblish_bumpybox import inventory class ValidateOutputRange(api.InstancePlugin): @@ -11,7 +10,7 @@ class ValidateOutputRange(api.InstancePlugin): do. """ - order = inventory.get_order(__file__, "ValidateOutputRange") + order = api.ValidatorOrder families = ["trackItem.task"] label = "Output Range" hosts = ["nukestudio"] @@ -40,7 +39,7 @@ class ValidateOutputRange(api.InstancePlugin): class ValidateImageSequence(api.InstancePlugin): """Validate image sequence output path is setup correctly.""" - order = inventory.get_order(__file__, "ValidateImageSequence") + order = api.ValidatorOrder families = ["trackItem.task", "img"] match = api.Subset label = "Image Sequence" diff --git a/pype/plugins/nukestudio/publish/validate_track_item.py b/pype/plugins/nukestudio/publish/validate_track_item.py index 3c8b3c6cfd..3fe7a739ce 100644 --- a/pype/plugins/nukestudio/publish/validate_track_item.py +++ b/pype/plugins/nukestudio/publish/validate_track_item.py @@ -1,6 +1,4 @@ from pyblish import api -from pyblish_bumpybox import inventory - class ValidateTrackItem(api.InstancePlugin): """Validate the track item to the sequence. @@ -8,10 +6,10 @@ class ValidateTrackItem(api.InstancePlugin): Exact matching to optimize processing. """ - order = inventory.get_order(__file__, "ValidateTrackItem") + order = api.ValidatorOrder families = ["trackItem"] match = api.Exact - label = "Track Item" + label = "Validate Track Item" hosts = ["nukestudio"] optional = True @@ -21,7 +19,7 @@ class ValidateTrackItem(api.InstancePlugin): self.log.info("__ item: {}".format(item)) media_source = item.source().mediaSource() self.log.info("__ media_source: {}".format(media_source)) - + msg = ( 'A setting does not match between track item "{0}" and sequence ' '"{1}".'.format(item.name(), item.sequence().name()) + @@ -55,5 +53,5 @@ class ValidateTrackItem(api.InstancePlugin): # accommodate for the ftrack family addition. # """ # -# order = inventory.get_order(__file__, "ValidateTrackItemFtrack") +# order = api.ValidatorOrder # families = ["trackItem", "ftrack"] diff --git a/pype/plugins/nukestudio/publish/validate_viewer_lut.py b/pype/plugins/nukestudio/publish/validate_viewer_lut.py index c9dc87a95b..08c084880d 100644 --- a/pype/plugins/nukestudio/publish/validate_viewer_lut.py +++ b/pype/plugins/nukestudio/publish/validate_viewer_lut.py @@ -1,11 +1,10 @@ from pyblish import api -from pyblish_bumpybox import inventory class ValidateViewerLut(api.ContextPlugin): """Validate viewer lut in NukeStudio is the same as in Nuke.""" - order = inventory.get_order(__file__, "ValidateViewerLut") + order = api.ValidatorOrder label = "Viewer LUT" hosts = ["nukestudio"] optional = True From ba6ed0c1481d38e0d0d22c066258a022aa713bd9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 12 May 2019 22:26:27 +0200 Subject: [PATCH 160/193] add standalone publisher specific plugin temporarily due to certain specifics in the standalone vs in app publishing --- .../publish/collect_context.py | 15 +- .../publish/collect_ftrack_api.py | 40 ++ .../standalonepublish/publish/integrate.py | 73 +-- .../publish/integrate_ftrack_api.py | 315 +++++++++++++ .../publish/integrate_ftrack_instances.py | 101 ++++ .../publish/integrate_rendered_frames.py | 436 ++++++++++++++++++ 6 files changed, 951 insertions(+), 29 deletions(-) create mode 100644 pype/plugins/standalonepublish/publish/collect_ftrack_api.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_ftrack_api.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_rendered_frames.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 26ebc642fb..cbe9df1ef6 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -6,6 +6,7 @@ from avalon import ( ) import json import logging +import clique log = logging.getLogger("collector") @@ -65,12 +66,22 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instance.data["files"] = list() instance.data['destination_list'] = list() + instance.data['representations'] = list() for component in in_data['representations']: # instance.add(node) + component['destination'] = component['files'] + collections, remainder = clique.assemble(component['files']) + if collections: + self.log.debug(collections) + range = collections[0].format('{range}') + instance.data['startFrame'] = range.split('-')[0] + instance.data['endFrame'] = range.split('-')[1] + + + instance.data["files"].append(component) + instance.data["representations"].append(component) - instance.data["files"].append(component['files']) - instance.data['destination_list'].append(component['files']) # "is_thumbnail": component['thumbnail'], # "is_preview": component['preview'] diff --git a/pype/plugins/standalonepublish/publish/collect_ftrack_api.py b/pype/plugins/standalonepublish/publish/collect_ftrack_api.py new file mode 100644 index 0000000000..6df998350c --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_ftrack_api.py @@ -0,0 +1,40 @@ +import os +import pyblish.api + +try: + import ftrack_api_old as ftrack_api +except Exception: + import ftrack_api + + +class CollectFtrackApi(pyblish.api.ContextPlugin): + """ Collects an ftrack session and the current task id. """ + + order = pyblish.api.CollectorOrder + label = "Collect Ftrack Api" + + def process(self, context): + + # Collect session + session = ftrack_api.Session() + context.data["ftrackSession"] = session + + # Collect task + + project = os.environ.get('AVALON_PROJECT', '') + asset = os.environ.get('AVALON_ASSET', '') + task = os.environ.get('AVALON_TASK', None) + + if task: + result = session.query('Task where\ + project.full_name is "{0}" and\ + name is "{1}" and\ + parent.name is "{2}"'.format(project, task, asset)).one() + context.data["ftrackTask"] = result + else: + result = session.query('TypedContext where\ + project.full_name is "{0}" and\ + name is "{1}"'.format(project, asset)).one() + context.data["ftrackEntity"] = result + + self.log.info(result) diff --git a/pype/plugins/standalonepublish/publish/integrate.py b/pype/plugins/standalonepublish/publish/integrate.py index 7157a6fb1a..b6771a52e0 100644 --- a/pype/plugins/standalonepublish/publish/integrate.py +++ b/pype/plugins/standalonepublish/publish/integrate.py @@ -6,6 +6,7 @@ import errno import pyblish.api from avalon import api, io from avalon.vendor import filelink +import clique log = logging.getLogger(__name__) @@ -39,13 +40,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "yetiRig", "yeticache", "nukescript", - "review", + # "review", "workfile", "scene", "ass"] exclude_families = ["clip"] def process(self, instance): + if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return @@ -140,7 +142,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Creating version ...") version_id = io.insert_one(version).inserted_id - + instance.data['version'] = version['name'] # Write to disk # _ # | | @@ -181,7 +183,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if 'transfers' not in instance.data: instance.data['transfers'] = [] - for files in instance.data["files"]: + for idx, repre in enumerate(instance.data["representations"]): # Collection # _______ @@ -193,27 +195,43 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # |_______| # - if isinstance(files, list): - collection = files + files = repre['files'] + + if len(files) > 1: + src_collections, remainder = clique.assemble(files) + self.log.debug("dst_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] # Assert that each member has identical suffix - _, ext = os.path.splitext(collection[0]) - assert all(ext == os.path.splitext(name)[1] - for name in collection), ( - "Files had varying suffixes, this is a bug" - ) + src_head = src_collection.format("{head}") + src_tail = ext = src_collection.format("{tail}") - # assert not any(os.path.isabs(name) for name in collection) - - template_data["representation"] = ext[1:] - - for fname in collection: - - src = fname + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = src_tail[1:] + template_data["frame"] = src_collection.format( + "{padding}") % i anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled["publish"]["path"] + test_dest_files.append(anatomy_filled["publish"]["path"]) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + instance.data["representations"][idx]['published_path'] = dst_collection.format() + + for i in src_collection.indexes: + src_padding = src_collection.format("{padding}") % i + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + dst_padding = dst_collection.format("{padding}") % i + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + + # src = os.path.join(stagingdir, src_file_name) + src = src_file_name + self.log.debug("source: {}".format(src)) instance.data["transfers"].append([src, dst]) - template = anatomy.templates["publish"]["path"] else: # Single file @@ -224,13 +242,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # | | # |_______| # - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) - _, ext = os.path.splitext(fname) + fname = files[0] + # assert not os.path.isabs(fname), ( + # "Given file name is a full path" + # ) + # _, ext = os.path.splitext(fname) - template_data["representation"] = ext[1:] + template_data["representation"] = repre['representation'] # src = os.path.join(stagingdir, fname) src = fname @@ -239,12 +257,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) template = anatomy.templates["publish"]["path"] + instance.data["representations"][idx]['published_path'] = dst representation = { "schema": "pype:representation-2.0", "type": "representation", "parent": version_id, - "name": ext[1:], + "name": repre['representation'], "data": {'path': dst, 'template': template}, "dependencies": instance.data.get("dependencies", "").split(), @@ -261,7 +280,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "subset": subset["name"], "version": version["name"], "hierarchy": hierarchy, - "representation": ext[1:] + # "representation": repre['representation'] } } diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py new file mode 100644 index 0000000000..9eff10ba67 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py @@ -0,0 +1,315 @@ +import os +import sys +import pyblish.api +import clique + + +class IntegrateFtrackApi(pyblish.api.InstancePlugin): + """ Commit components to server. """ + + order = pyblish.api.IntegratorOrder+0.499 + label = "Integrate Ftrack Api" + families = ["ftrack"] + + def query(self, entitytype, data): + """ Generate a query expression from data supplied. + + If a value is not a string, we'll add the id of the entity to the + query. + + Args: + entitytype (str): The type of entity to query. + data (dict): The data to identify the entity. + exclusions (list): All keys to exclude from the query. + + Returns: + str: String query to use with "session.query" + """ + queries = [] + if sys.version_info[0] < 3: + for key, value in data.iteritems(): + if not isinstance(value, (basestring, int)): + self.log.info("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + else: + for key, value in data.items(): + if not isinstance(value, (str, int)): + self.log.info("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + + query = ( + "select id from " + entitytype + " where " + " and ".join(queries) + ) + self.log.debug(query) + return query + + def process(self, instance): + + session = instance.context.data["ftrackSession"] + if instance.context.data.get("ftrackTask"): + task = instance.context.data["ftrackTask"] + name = task['full_name'] + parent = task["parent"] + elif instance.context.data.get("ftrackEntity"): + task = None + name = instance.context.data.get("ftrackEntity")['name'] + parent = instance.context.data.get("ftrackEntity") + + info_msg = "Created new {entity_type} with data: {data}" + info_msg += ", metadata: {metadata}." + + # Iterate over components and publish + for data in instance.data.get("ftrackComponentsList", []): + + # AssetType + # Get existing entity. + assettype_data = {"short": "upload"} + assettype_data.update(data.get("assettype_data", {})) + self.log.debug("data: {}".format(data)) + + assettype_entity = session.query( + self.query("AssetType", assettype_data) + ).first() + + # Create a new entity if none exits. + if not assettype_entity: + assettype_entity = session.create("AssetType", assettype_data) + self.log.debug( + "Created new AssetType with data: ".format(assettype_data) + ) + + # Asset + # Get existing entity. + asset_data = { + "name": name, + "type": assettype_entity, + "parent": parent, + } + asset_data.update(data.get("asset_data", {})) + + asset_entity = session.query( + self.query("Asset", asset_data) + ).first() + + self.log.info("asset entity: {}".format(asset_entity)) + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + asset_metadata = asset_data.pop("metadata", {}) + + # Create a new entity if none exits. + if not asset_entity: + asset_entity = session.create("Asset", asset_data) + self.log.debug( + info_msg.format( + entity_type="Asset", + data=asset_data, + metadata=asset_metadata + ) + ) + + # Adding metadata + existing_asset_metadata = asset_entity["metadata"] + existing_asset_metadata.update(asset_metadata) + asset_entity["metadata"] = existing_asset_metadata + + # AssetVersion + # Get existing entity. + assetversion_data = { + "version": 0, + "asset": asset_entity, + } + if task: + assetversion_data['task'] = task + + assetversion_data.update(data.get("assetversion_data", {})) + + assetversion_entity = session.query( + self.query("AssetVersion", assetversion_data) + ).first() + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + assetversion_metadata = assetversion_data.pop("metadata", {}) + + # Create a new entity if none exits. + if not assetversion_entity: + assetversion_entity = session.create( + "AssetVersion", assetversion_data + ) + self.log.debug( + info_msg.format( + entity_type="AssetVersion", + data=assetversion_data, + metadata=assetversion_metadata + ) + ) + + # Adding metadata + existing_assetversion_metadata = assetversion_entity["metadata"] + existing_assetversion_metadata.update(assetversion_metadata) + assetversion_entity["metadata"] = existing_assetversion_metadata + + # Have to commit the version and asset, because location can't + # determine the final location without. + session.commit() + + # Component + # Get existing entity. + component_data = { + "name": "main", + "version": assetversion_entity + } + component_data.update(data.get("component_data", {})) + + component_entity = session.query( + self.query("Component", component_data) + ).first() + + component_overwrite = data.get("component_overwrite", False) + location = data.get("component_location", session.pick_location()) + + # Overwrite existing component data if requested. + if component_entity and component_overwrite: + + origin_location = session.query( + "Location where name is \"ftrack.origin\"" + ).one() + + # Removing existing members from location + components = list(component_entity.get("members", [])) + components += [component_entity] + for component in components: + for loc in component["component_locations"]: + if location["id"] == loc["location_id"]: + location.remove_component( + component, recursive=False + ) + + # Deleting existing members on component entity + for member in component_entity.get("members", []): + session.delete(member) + del(member) + + session.commit() + + # Reset members in memory + if "members" in component_entity.keys(): + component_entity["members"] = [] + + # Add components to origin location + try: + collection = clique.parse(data["component_path"]) + except ValueError: + # Assume its a single file + # Changing file type + name, ext = os.path.splitext(data["component_path"]) + component_entity["file_type"] = ext + + origin_location.add_component( + component_entity, data["component_path"] + ) + else: + # Changing file type + component_entity["file_type"] = collection.format("{tail}") + + # Create member components for sequence. + for member_path in collection: + + size = 0 + try: + size = os.path.getsize(member_path) + except OSError: + pass + + name = collection.match(member_path).group("index") + + member_data = { + "name": name, + "container": component_entity, + "size": size, + "file_type": os.path.splitext(member_path)[-1] + } + + component = session.create( + "FileComponent", member_data + ) + origin_location.add_component( + component, member_path, recursive=False + ) + component_entity["members"].append(component) + + # Add components to location. + location.add_component( + component_entity, origin_location, recursive=True + ) + + data["component"] = component_entity + msg = "Overwriting Component with path: {0}, data: {1}, " + msg += "location: {2}" + self.log.info( + msg.format( + data["component_path"], + component_data, + location + ) + ) + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + component_metadata = component_data.pop("metadata", {}) + + # Create new component if none exists. + new_component = False + if not component_entity: + component_entity = assetversion_entity.create_component( + data["component_path"], + data=component_data, + location=location + ) + data["component"] = component_entity + msg = "Created new Component with path: {0}, data: {1}" + msg += ", metadata: {2}, location: {3}" + self.log.info( + msg.format( + data["component_path"], + component_data, + component_metadata, + location + ) + ) + new_component = True + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + + # if component_data['name'] = 'ftrackreview-mp4-mp4': + # assetversion_entity["thumbnail_id"] + + # Setting assetversion thumbnail + if data.get("thumbnail", False): + assetversion_entity["thumbnail_id"] = component_entity["id"] + + # Inform user about no changes to the database. + if (component_entity and not component_overwrite and + not new_component): + data["component"] = component_entity + self.log.info( + "Found existing component, and no request to overwrite. " + "Nothing has been changed." + ) + else: + # Commit changes. + session.commit() diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py new file mode 100644 index 0000000000..8d938bceb0 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py @@ -0,0 +1,101 @@ +import pyblish.api +import os +import json + + +class IntegrateFtrackInstance(pyblish.api.InstancePlugin): + """Collect ftrack component data + + Add ftrack component list to instance. + + + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = 'Integrate Ftrack Component' + families = ["ftrack"] + + family_mapping = {'camera': 'cam', + 'look': 'look', + 'mayaAscii': 'scene', + 'model': 'geo', + 'rig': 'rig', + 'setdress': 'setdress', + 'pointcache': 'cache', + 'write': 'img', + 'render': 'render', + 'nukescript': 'comp', + 'review': 'mov'} + + def process(self, instance): + self.log.debug('instance {}'.format(instance)) + + if instance.data.get('version'): + version_number = int(instance.data.get('version')) + + family = instance.data['family'].lower() + + asset_type = '' + asset_type = self.family_mapping[family] + + componentList = [] + ft_session = instance.context.data["ftrackSession"] + + components = instance.data['representations'] + + for comp in components: + self.log.debug('component {}'.format(comp)) + # filename, ext = os.path.splitext(file) + # self.log.debug('dest ext: ' + ext) + + # ext = comp['Context'] + + if comp['thumbnail']: + location = ft_session.query( + 'Location where name is "ftrack.server"').one() + component_data = { + "name": "thumbnail" # Default component name is "main". + } + elif comp['preview']: + if not instance.data.get('startFrameReview'): + instance.data['startFrameReview'] = instance.data['startFrame'] + if not instance.data.get('endFrameReview'): + instance.data['endFrameReview'] = instance.data['endFrame'] + location = ft_session.query( + 'Location where name is "ftrack.server"').one() + component_data = { + # Default component name is "main". + "name": "ftrackreview-mp4", + "metadata": {'ftr_meta': json.dumps({ + 'frameIn': int(instance.data['startFrameReview']), + 'frameOut': int(instance.data['endFrameReview']), + 'frameRate': 25.0})} + } + else: + component_data = { + "name": comp['representation'] # Default component name is "main". + } + location = ft_session.query( + 'Location where name is "ftrack.unmanaged"').one() + + self.log.debug('location {}'.format(location)) + + componentList.append({"assettype_data": { + "short": asset_type, + }, + "asset_data": { + "name": instance.data["subset"], + }, + "assetversion_data": { + "version": version_number, + }, + "component_data": component_data, + "component_path": comp['published_path'], + 'component_location': location, + "component_overwrite": False, + "thumbnail": comp['thumbnail'] + } + ) + + self.log.debug('componentsList: {}'.format(str(componentList))) + instance.data["ftrackComponentsList"] = componentList diff --git a/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py b/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py new file mode 100644 index 0000000000..43653ab0ed --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py @@ -0,0 +1,436 @@ +import os +import logging +import shutil +import clique + +import errno +import pyblish.api +from avalon import api, io + + +log = logging.getLogger(__name__) + + +class IntegrateFrames(pyblish.api.InstancePlugin): + """Resolve any dependency issies + + This plug-in resolves any paths which, if not updated might break + the published file. + + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + """ + + label = "Integrate Frames" + order = pyblish.api.IntegratorOrder + families = [ + "imagesequence", + "render", + "write", + "source", + 'review'] + + family_targets = [".frames", ".local", ".review", "review", "imagesequence", "render", "source"] + exclude_families = ["clip"] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + + families = [f for f in instance.data["families"] + for search in self.family_targets + if search in f] + + if not families: + return + + self.register(instance) + + # self.log.info("Integrating Asset in to the database ...") + # self.log.info("instance.data: {}".format(instance.data)) + if instance.data.get('transfer', True): + self.integrate(instance) + + def register(self, instance): + + # Required environment variables + PROJECT = api.Session["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"] + LOCATION = api.Session["AVALON_LOCATION"] + + context = instance.context + # Atomicity + # + # Guarantee atomic publishes - each asset contains + # an identical set of members. + # __ + # / o + # / \ + # | o | + # \ / + # o __/ + # + assert all(result["success"] for result in context.data["results"]), ( + "Atomicity not held, aborting.") + + # Assemble + # + # | + # v + # ---> <---- + # ^ + # | + # + # stagingdir = instance.data.get("stagingDir") + # assert stagingdir, ("Incomplete instance \"%s\": " + # "Missing reference to staging area." % instance) + + # extra check if stagingDir actually exists and is available + + # self.log.debug("Establishing staging directory @ %s" % stagingdir) + + project = io.find_one({"type": "project"}) + + asset = io.find_one({"type": "asset", + "name": ASSET, + "parent": project["_id"]}) + + assert all([project, asset]), ("Could not find current project or " + "asset '%s'" % ASSET) + + subset = self.get_subset(asset, instance) + + # get next version + latest_version = io.find_one({"type": "version", + "parent": subset["_id"]}, + {"name": True}, + sort=[("name", -1)]) + + next_version = 1 + if latest_version is not None: + next_version += latest_version["name"] + + self.log.info("Verifying version from assumed destination") + + # assumed_data = instance.data["assumedTemplateData"] + # assumed_version = assumed_data["version"] + # if assumed_version != next_version: + # raise AttributeError("Assumed version 'v{0:03d}' does not match" + # "next version in database " + # "('v{1:03d}')".format(assumed_version, + # next_version)) + + if instance.data.get('version'): + next_version = int(instance.data.get('version')) + + instance.data['version'] = next_version + + self.log.debug("Next version: v{0:03d}".format(next_version)) + + version_data = self.create_version_data(context, instance) + version = self.create_version(subset=subset, + version_number=next_version, + locations=[LOCATION], + data=version_data) + + self.log.debug("Creating version ...") + version_id = io.insert_one(version).inserted_id + + # Write to disk + # _ + # | | + # _| |_ + # ____\ / + # |\ \ / \ + # \ \ v \ + # \ \________. + # \|________| + # + root = api.registered_root() + hierarchy = "" + parents = io.find_one({"type": 'asset', "name": ASSET})[ + 'data']['parents'] + if parents and len(parents) > 0: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*parents) + + template_data = {"root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + "silo": asset['silo'], + "task": api.Session["AVALON_TASK"], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy} + + # template_publish = project["config"]["template"]["publish"] + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + # for repre in instance.data["representations"]: + for idx, repre in enumerate(instance.data["representations"]): + # Collection + # _______ + # |______|\ + # | |\| + # | || + # | || + # | || + # |_______| + # + + files = repre['files'] + + if len(files) > 1: + + src_collections, remainder = clique.assemble(files) + self.log.debug("dst_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] + # Assert that each member has identical suffix + src_head = src_collection.format("{head}") + src_tail = ext = src_collection.format("{tail}") + + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = repre['representation'] + template_data["frame"] = src_collection.format( + "{padding}") % i + anatomy_filled = anatomy.format(template_data) + test_dest_files.append(anatomy_filled["render"]["path"]) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + instance.data["representations"][idx]['published_path'] = dst_collection.format() + + for i in src_collection.indexes: + src_padding = src_collection.format("{padding}") % i + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + dst_padding = dst_collection.format("{padding}") % i + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + + # src = os.path.join(stagingdir, src_file_name) + src = src_file_name + self.log.debug("source: {}".format(src)) + + instance.data["transfers"].append([src, dst]) + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + + template_data.pop("frame", None) + + fname = files[0] + + self.log.info("fname: {}".format(fname)) + + # assert not os.path.isabs(fname), ( + # "Given file name is a full path" + # ) + # _, ext = os.path.splitext(fname) + + template_data["representation"] = repre['representation'] + + # src = os.path.join(stagingdir, fname) + src = src_file_name + + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["render"]["path"] + + instance.data["transfers"].append([src, dst]) + instance.data["representations"][idx]['published_path'] = dst + + if repre['ext'] not in ["jpeg", "jpg", "mov", "mp4", "wav"]: + template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) + + anatomy_filled = anatomy.format(template_data) + path_to_save = anatomy_filled["render"]["path"] + template = anatomy.templates["render"]["path"] + + self.log.debug("path_to_save: {}".format(path_to_save)) + + representation = { + "schema": "pype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": repre['representation'], + "data": {'path': path_to_save, 'template': template}, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": { + "root": root, + "project": { + "name": PROJECT, + "code": project['data']['code'] + }, + "task": api.Session["AVALON_TASK"], + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy, + "representation": repre['representation'] + } + } + + destination_list.append(dst) + instance.data['destination_list'] = destination_list + representations.append(representation) + + self.log.info("Registering {} items".format(len(representations))) + io.insert_many(representations) + + def integrate(self, instance): + """Move the files + + Through `instance.data["transfers"]` + + Args: + instance: the instance to integrate + """ + + transfers = instance.data["transfers"] + + for src, dest in transfers: + src = os.path.normpath(src) + dest = os.path.normpath(dest) + if src in dest: + continue + + self.log.info("Copying file .. {} -> {}".format(src, dest)) + self.copy_file(src, dest) + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + shutil.copy(src, dst) + + def get_subset(self, asset, instance): + + subset = io.find_one({"type": "subset", + "parent": asset["_id"], + "name": instance.data["subset"]}) + + if subset is None: + subset_name = instance.data["subset"] + self.log.info("Subset '%s' not found, creating.." % subset_name) + + _id = io.insert_one({ + "schema": "pype:subset-2.0", + "type": "subset", + "name": subset_name, + "data": {}, + "parent": asset["_id"] + }).inserted_id + + subset = io.find_one({"_id": _id}) + + return subset + + def create_version(self, subset, version_number, locations, data=None): + """ Copy given source to destination + + Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + locations (list): the currently registered locations + + Returns: + dict: collection of data to create a version + """ + # Imprint currently registered location + version_locations = [location for location in locations if + location is not None] + + return {"schema": "pype:version-2.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "locations": version_locations, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context + instance: the current instance being published + + Returns: + dict: the required information with instance.data as key + """ + + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families + + # try: + # source = instance.data['source'] + # except KeyError: + # source = context.data["currentFile"] + # + # relative_path = os.path.relpath(source, api.registered_root()) + # source = os.path.join("{root}", relative_path).replace("\\", "/") + + source = "standalone" + + version_data = {"families": families, + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment")} + + # Include optional data if present in + optionals = ["startFrame", "endFrame", "step", + "handles", "colorspace", "fps", "outputDir"] + + for key in optionals: + if key in instance.data: + version_data[key] = instance.data.get(key, None) + + return version_data From 93e99e41c657be07822e18fef00e024d37cf0d74 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 12 May 2019 22:27:12 +0200 Subject: [PATCH 161/193] fix hierarchy spelling --- pype/standalonepublish/publish.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 11c2f353d1..4442f0243c 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -23,7 +23,7 @@ PUBLISH_PATH = os.path.sep.join( ) pyblish.api.register_plugin_path(PUBLISH_PATH) -# Registers Standalone pyblish plugins +# # Registers Standalone pyblish plugins # PUBLISH_PATH = os.path.sep.join( # [pype.PLUGINS_DIR, 'ftrack', 'publish'] # ) @@ -55,8 +55,8 @@ def set_context(project, asset, app): if parents and len(parents) > 0: hierarchy = os.path.sep.join(parents) - os.environ["AVALON_HIEARCHY"] = hierarchy - io.Session["AVALON_HIEARCHY"] = hierarchy + os.environ["AVALON_HIERARCHY"] = hierarchy + io.Session["AVALON_HIERARCHY"] = hierarchy os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') From c8395f43dda60a79b51ab56643d6dee9b22c5e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Sun, 12 May 2019 22:41:40 +0000 Subject: [PATCH 162/193] fix(ftrack): check if asset is in avalon db before trying to delete it, throw sanitized error otherwise --- pype/ftrack/actions/action_delete_asset.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index eabadecee6..96087f4c8e 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -85,6 +85,12 @@ class DeleteAsset(BaseAction): 'type': 'asset', 'name': entity['name'] }) + + if av_entity is None: + return { + 'success': False, + 'message': 'Didn\'t found assets in avalon' + } asset_label = { 'type': 'label', From a48af95ffe2f7f3377d5899049fff31fa39d4348 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 16:32:39 +0200 Subject: [PATCH 163/193] root for launcher in avalon_apps is not parsed from arguments --- pype/avalon_apps/avalon_app.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pype/avalon_apps/avalon_app.py b/pype/avalon_apps/avalon_app.py index 0b2553c4d9..547ecd2299 100644 --- a/pype/avalon_apps/avalon_app.py +++ b/pype/avalon_apps/avalon_app.py @@ -40,15 +40,7 @@ class AvalonApps: def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window if self.app_launcher is None: - parser = argparse.ArgumentParser() - parser.add_argument("--demo", action="store_true") - parser.add_argument( - "--root", default=os.environ["AVALON_PROJECTS"] - ) - kwargs = parser.parse_args() - - root = kwargs.root - root = os.path.realpath(root) + root = os.path.realpath(os.environ["AVALON_PROJECTS"]) io.install() APP_PATH = launcher_lib.resource("qml", "main.qml") self.app_launcher = launcher_widget.Launcher(root, APP_PATH) From 1b33c88ca58156d276fb6bda1f628453540c500a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:04:32 +0200 Subject: [PATCH 164/193] component also returns start/end frame and frame rate (fps) if have these info in input data --- .../widgets/widget_component_item.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 2e0df9a00c..14f6a8312d 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -10,11 +10,16 @@ class ComponentItem(QtWidgets.QFrame): C_HOVER = '#ffffff' C_ACTIVE = '#4BB543' C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) signal_repre_change = QtCore.Signal(object, object) + startFrame = None + endFrame = None + frameRate = None + def __init__(self, parent, main_parent): super().__init__() self.has_valid_repre = True @@ -291,4 +296,12 @@ class ComponentItem(QtWidgets.QFrame): 'thumbnail': self.is_thumbnail(), 'preview': self.is_preview() } + + if ('startFrame' in self.in_data and 'endFrame' in self.in_data): + data['startFrame'] = self.in_data['startFrame'] + data['endFrame'] = self.in_data['endFrame'] + + if 'frameRate' in self.in_data: + data['frameRate'] = self.in_data['frameRate'] + return data From 9000ca0b83eb94c20ccc390e6a8b730b59df9833 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:05:41 +0200 Subject: [PATCH 165/193] removed get_ranges (death code) --- .../widgets/widget_drop_frame.py | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index cffe673152..de2bbe19a3 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -182,34 +182,8 @@ class DropDataFrame(QtWidgets.QFrame): 'is_sequence': True, 'actions': actions } - self._process_data(data) - def _get_ranges(self, indexes): - if len(indexes) == 1: - return str(indexes[0]) - ranges = [] - first = None - last = None - for index in indexes: - if first is None: - first = index - last = index - elif (last+1) == index: - last = index - else: - if first == last: - range = str(first) - else: - range = '{}-{}'.format(first, last) - ranges.append(range) - first = index - last = index - if first == last: - range = str(first) - else: - range = '{}-{}'.format(first, last) - ranges.append(range) - return ', '.join(ranges) + self._process_data(data) def _process_remainder(self, remainder): filename = os.path.basename(remainder) From 56d1b8d1e48f2d3d9c82ec6adede46cf196b7f06 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:06:14 +0200 Subject: [PATCH 166/193] pasted path from clipboard is normpathed --- pype/standalonepublish/widgets/widget_drop_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index de2bbe19a3..6be69584d0 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -49,7 +49,7 @@ class DropDataFrame(QtWidgets.QFrame): else: # If path is in clipboard as string try: - path = ent.text() + path = os.path.normpath(ent.text()) if os.path.exists(path): paths.append(path) else: From da376847404bc61d0fc9d5b37be60e5ca52fa5f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:06:40 +0200 Subject: [PATCH 167/193] collections add start and end frame into data --- pype/standalonepublish/widgets/widget_drop_frame.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 6be69584d0..2fd14c26c7 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -170,6 +170,13 @@ class DropDataFrame(QtWidgets.QFrame): repr_name = file_ext.replace('.', '') range = collection.format('{ranges}') + # TODO: ranges must not be with missing frames!!! + # - this is goal implementation: + # startFrame, endFrame = range.split('-') + rngs = range.split(',') + startFrame = rngs[0].split('-')[0] + endFrame = rngs[-1].split('-')[-1] + actions = [] data = { @@ -177,6 +184,8 @@ class DropDataFrame(QtWidgets.QFrame): 'name': file_base, 'ext': file_ext, 'file_info': range, + 'startFrame': startFrame, + 'endFrame': endFrame, 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': True, From 9c0c3a346559d1c2e9b495ca4b7c75959c441549 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:07:45 +0200 Subject: [PATCH 168/193] added method for enhanced getting data from ffprobe --- .../widgets/widget_drop_frame.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 2fd14c26c7..1fe2777826 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -1,5 +1,6 @@ import os import re +import json import clique import subprocess from pypeapp import config @@ -244,6 +245,25 @@ class DropDataFrame(QtWidgets.QFrame): break except Exception as e: pass + def load_data_with_probe(self, filepath): + args = [ + 'ffprobe', + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + '-show_streams', filepath + ] + ffprobe_p = subprocess.Popen( + args, + stdout=subprocess.PIPE, + shell=True + ) + ffprobe_output = ffprobe_p.communicate()[0] + if ffprobe_p.returncode != 0: + raise RuntimeError( + 'Failed on ffprobe: check if ffprobe path is set in PATH env' + ) + return json.loads(ffprobe_output)['streams'][0] return output def _process_data(self, data): From ec38c4b048307a4b97afe29f7dcb2b4018aa6eb1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:08:44 +0200 Subject: [PATCH 169/193] get file_info replaced with get_file_data which collect more information --- .../widgets/widget_drop_frame.py | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 1fe2777826..4e99f697cb 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -216,35 +216,9 @@ class DropDataFrame(QtWidgets.QFrame): 'is_sequence': False, 'actions': actions } - data['file_info'] = self.get_file_info(data) self._process_data(data) - def get_file_info(self, data): - output = None - if data['ext'] == '.mov': - try: - # ffProbe must be in PATH - filepath = data['files'][0] - args = ['ffprobe', '-show_streams', filepath] - p = subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True - ) - datalines=[] - for line in iter(p.stdout.readline, b''): - line = line.decode("utf-8").replace('\r\n', '') - datalines.append(line) - - find_value = 'codec_name' - for line in datalines: - if line.startswith(find_value): - output = line.replace(find_value + '=', '') - break - except Exception as e: - pass def load_data_with_probe(self, filepath): args = [ 'ffprobe', @@ -264,10 +238,53 @@ class DropDataFrame(QtWidgets.QFrame): 'Failed on ffprobe: check if ffprobe path is set in PATH env' ) return json.loads(ffprobe_output)['streams'][0] + + def get_file_data(self, data): + filepath = data['files'][0] + ext = data['ext'] + output = {} + probe_data = self.load_data_with_probe(filepath) + + if ( + ext in self.presets['extensions']['image_file'] or + ext in self.presets['extensions']['video_file'] + ): + if 'frameRate' not in data: + # default value + frameRate = 25 + frameRate_string = probe_data.get('r_frame_rate') + if frameRate_string: + frameRate = int(frameRate_string.split('/')[0]) + + output['frameRate'] = frameRate + + if 'startFrame' not in data or 'endFrame' not in data: + startFrame = endFrame = 1 + endFrame_string = probe_data.get('nb_frames') + + if endFrame_string: + endFrame = int(endFrame_string) + + output['startFrame'] = startFrame + output['endFrame'] = endFrame + + file_info = None + if 'file_info' in data: + file_info = data['file_info'] + elif ext in ['.mov']: + file_info = probe_data.get('codec_name') + + output['file_info'] = file_info + return output def _process_data(self, data): ext = data['ext'] + # load file data info + file_data = self.get_file_data(data) + for key, value in file_data.items(): + data[key] = value + icon = 'default' for ico, exts in self.presets['extensions'].items(): if ext in exts: From 13d551ba72f5d3b663ff804ac9033f33d6454ceb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 May 2019 19:10:25 +0200 Subject: [PATCH 170/193] removed not used code --- pype/standalonepublish/widgets/widget_component_item.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 14f6a8312d..43aa54a955 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -16,10 +16,6 @@ class ComponentItem(QtWidgets.QFrame): signal_preview = QtCore.Signal(object) signal_repre_change = QtCore.Signal(object, object) - startFrame = None - endFrame = None - frameRate = None - def __init__(self, parent, main_parent): super().__init__() self.has_valid_repre = True From 08712b4cb5e228227e8e81a70ef0e3a021acd714 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 14:49:22 +0200 Subject: [PATCH 171/193] version spinbox added to middle widget so user can choose version --- pype/standalonepublish/widgets/__init__.py | 5 +- .../widgets/widget_family.py | 62 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py index cd99e15bed..4c6a0e85a5 100644 --- a/pype/standalonepublish/widgets/__init__.py +++ b/pype/standalonepublish/widgets/__init__.py @@ -20,15 +20,14 @@ from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView from .widget_asset import AssetWidget + from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget from .widget_drop_empty import DropEmpty from .widget_component_item import ComponentItem from .widget_components_list import ComponentsList - from .widget_drop_frame import DropDataFrame - from .widget_components import ComponentsWidget -from.widget_shadow import ShadowWidget +from .widget_shadow import ShadowWidget diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 7259ecdb64..a2276bf7f9 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -51,6 +51,19 @@ class FamilyWidget(QtWidgets.QWidget): name_layout.addWidget(btn_subset) name_layout.setContentsMargins(0, 0, 0, 0) + # version + version_spinbox = QtWidgets.QSpinBox() + version_spinbox.setMinimum(1) + version_spinbox.setMaximum(9999) + version_spinbox.setEnabled(False) + + version_checkbox = QtWidgets.QCheckBox("Next Available Version") + version_checkbox.setCheckState(QtCore.Qt.CheckState(2)) + + version_layout = QtWidgets.QHBoxLayout() + version_layout.addWidget(version_spinbox) + version_layout.addWidget(version_checkbox) + layout = QtWidgets.QVBoxLayout(container) header = FamilyDescriptionWidget(self) @@ -63,6 +76,8 @@ class FamilyWidget(QtWidgets.QWidget): layout.addWidget(QtWidgets.QLabel("Subset")) layout.addLayout(name_layout) layout.addWidget(input_result) + layout.addWidget(QtWidgets.QLabel("Version")) + layout.addLayout(version_layout) layout.setContentsMargins(0, 0, 0, 0) options = QtWidgets.QWidget() @@ -86,6 +101,7 @@ class FamilyWidget(QtWidgets.QWidget): input_asset.textChanged.connect(self.on_data_changed) list_families.currentItemChanged.connect(self.on_selection_changed) list_families.currentItemChanged.connect(header.set_item) + version_checkbox.stateChanged.connect(self.on_version_refresh) self.stateChanged.connect(self._on_state_changed) @@ -95,6 +111,8 @@ class FamilyWidget(QtWidgets.QWidget): self.list_families = list_families self.input_asset = input_asset self.input_result = input_result + self.version_checkbox = version_checkbox + self.version_spinbox = version_spinbox self.refresh() @@ -103,7 +121,8 @@ class FamilyWidget(QtWidgets.QWidget): family = plugin.family.rsplit(".", 1)[-1] data = { 'family': family, - 'subset': self.input_subset.text() + 'subset': self.input_subset.text(), + 'version': self.version_spinbox.value() } return data @@ -204,6 +223,8 @@ class FamilyWidget(QtWidgets.QWidget): if asset_name != self.parent_widget.NOT_SELECTED: self.echo("'%s' not found .." % asset_name) + self.on_version_refresh() + # Update the valid state valid = ( subset_name.strip() != "" and @@ -213,6 +234,45 @@ class FamilyWidget(QtWidgets.QWidget): ) self.stateChanged.emit(valid) + def on_version_refresh(self): + auto_version = self.version_checkbox.isChecked() + self.version_spinbox.setEnabled(not auto_version) + if not auto_version: + return + + version = 1 + + asset_name = self.input_asset.text() + subset_name = self.input_result.text() + if ( + ( + asset_name.strip() != '' or + asset_name == self.parent_widget.NOT_SELECTED + ) and subset_name.strip() != '' + ): + asset = self.db.find_one({ + 'type': 'asset', + 'name': asset_name + }) + subset = self.db.find_one({ + 'type': 'subset', + 'parent': asset['_id'], + 'name': subset_name + }) + if subset: + versions = self.db.find({ + 'type': 'version', + 'parent': subset['_id'] + }) + if versions: + versions = sorted( + [v for v in versions], + key=lambda ver: ver['name'] + ) + version = int(versions[-1]['name']) + 1 + + self.version_spinbox.setValue(version) + def on_data_changed(self, *args): # Set invalid state until it's reconfirmed to be valid by the From 58d941d76df7326887d97043dab7605abc7a2c24 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 15:16:20 +0200 Subject: [PATCH 172/193] fixed asset not selected bug --- pype/standalonepublish/widgets/widget_family.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index a2276bf7f9..9a347cbeab 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -245,10 +245,8 @@ class FamilyWidget(QtWidgets.QWidget): asset_name = self.input_asset.text() subset_name = self.input_result.text() if ( - ( - asset_name.strip() != '' or - asset_name == self.parent_widget.NOT_SELECTED - ) and subset_name.strip() != '' + asset_name != self.parent_widget.NOT_SELECTED and + subset_name.strip() != '' ): asset = self.db.find_one({ 'type': 'asset', From f472285f657f7e83cb7736c0d28d849ea6e29de2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 15:16:51 +0200 Subject: [PATCH 173/193] changed font color of not enabled inputs so its readable --- pype/standalonepublish/widgets/widget_family.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 9a347cbeab..102705f98b 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -35,7 +35,7 @@ class FamilyWidget(QtWidgets.QWidget): input_asset.setStyleSheet("color: #BBBBBB;") input_subset = QtWidgets.QLineEdit() input_result = QtWidgets.QLineEdit() - input_result.setStyleSheet("color: gray;") + input_result.setStyleSheet("color: #BBBBBB;") input_result.setEnabled(False) # region Menu for default subset names @@ -56,6 +56,7 @@ class FamilyWidget(QtWidgets.QWidget): version_spinbox.setMinimum(1) version_spinbox.setMaximum(9999) version_spinbox.setEnabled(False) + version_spinbox.setStyleSheet("color: #BBBBBB;") version_checkbox = QtWidgets.QCheckBox("Next Available Version") version_checkbox.setCheckState(QtCore.Qt.CheckState(2)) From 35e5e7319f8efe5db41998e52e90ab3c43a7e417 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 May 2019 15:43:58 +0200 Subject: [PATCH 174/193] feat(nukestudio): plugins work and basic Tags integration --- .../publish/collect_assumed_destination.py | 2 +- .../plugins/global/publish/collect_presets.py | 14 ++ .../validate_resolved_paths.py | 0 .../{publish => _unused}/validate_task.py | 0 .../plugins/nukestudio/publish/collectTags.py | 14 -- .../publish/{collect.py => collect_clips.py} | 24 +-- .../nukestudio/publish/collect_framerate.py | 13 ++ .../publish/collect_hierarchy_context.py | 72 ++++++++ .../nukestudio/publish/collect_metadata.py | 30 ++++ .../nukestudio/publish/collect_subsets.py | 45 +++++ .../nukestudio/publish/collect_tags.py | 30 ++++ .../publish/integrate_assumed_destination.py | 132 +++++++++++++++ .../integrate_ftrack_component_overwrite.py | 21 +++ .../publish/integrate_hierarchy_avalon.py | 140 ++++++++++++++++ .../publish/integrate_hierarchy_ftrack.py | 155 ++++++++++++++++++ .../nukestudio/publish/validate_names.py | 4 +- .../nukestudio/publish/validate_track_item.py | 15 +- .../Templates/SharedTags.hrox | 152 ++++++++--------- 18 files changed, 740 insertions(+), 123 deletions(-) create mode 100644 pype/plugins/global/publish/collect_presets.py rename pype/plugins/nukestudio/{publish => _unused}/validate_resolved_paths.py (100%) rename pype/plugins/nukestudio/{publish => _unused}/validate_task.py (100%) delete mode 100644 pype/plugins/nukestudio/publish/collectTags.py rename pype/plugins/nukestudio/publish/{collect.py => collect_clips.py} (68%) create mode 100644 pype/plugins/nukestudio/publish/collect_framerate.py create mode 100644 pype/plugins/nukestudio/publish/collect_hierarchy_context.py create mode 100644 pype/plugins/nukestudio/publish/collect_metadata.py create mode 100644 pype/plugins/nukestudio/publish/collect_subsets.py create mode 100644 pype/plugins/nukestudio/publish/collect_tags.py create mode 100644 pype/plugins/nukestudio/publish/integrate_assumed_destination.py create mode 100644 pype/plugins/nukestudio/publish/integrate_ftrack_component_overwrite.py create mode 100644 pype/plugins/nukestudio/publish/integrate_hierarchy_avalon.py create mode 100644 pype/plugins/nukestudio/publish/integrate_hierarchy_ftrack.py diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 4f9d180843..fa6a3d9423 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -9,7 +9,7 @@ class CollectAssumedDestination(pyblish.api.ContextPlugin): label = "Collect Assumed Destination" order = pyblish.api.CollectorOrder + 0.498 - exclude_families = ["clip", "trackItem"] + exclude_families = ["clip"] def process(self, context): for instance in context: diff --git a/pype/plugins/global/publish/collect_presets.py b/pype/plugins/global/publish/collect_presets.py new file mode 100644 index 0000000000..8edf9797de --- /dev/null +++ b/pype/plugins/global/publish/collect_presets.py @@ -0,0 +1,14 @@ +from pyblish import api +from pypeapp import config + + +class CollectPresets(api.ContextPlugin): + """Collect Presets.""" + + order = api.CollectorOrder + label = "Collect Presets" + + def process(self, context): + context.data["presets"] = config.get_presets() + self.log.info(context.data["presets"]) + return diff --git a/pype/plugins/nukestudio/publish/validate_resolved_paths.py b/pype/plugins/nukestudio/_unused/validate_resolved_paths.py similarity index 100% rename from pype/plugins/nukestudio/publish/validate_resolved_paths.py rename to pype/plugins/nukestudio/_unused/validate_resolved_paths.py diff --git a/pype/plugins/nukestudio/publish/validate_task.py b/pype/plugins/nukestudio/_unused/validate_task.py similarity index 100% rename from pype/plugins/nukestudio/publish/validate_task.py rename to pype/plugins/nukestudio/_unused/validate_task.py diff --git a/pype/plugins/nukestudio/publish/collectTags.py b/pype/plugins/nukestudio/publish/collectTags.py deleted file mode 100644 index 5137a9f22f..0000000000 --- a/pype/plugins/nukestudio/publish/collectTags.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyblish import api - - -class CollectTrackItemTags(api.InstancePlugin): - """Collect Tags from selected track items.""" - - order = api.CollectorOrder - label = "Collect Tags" - hosts = ["nukestudio"] - - def process(self, instance): - instance.data["tags"] = instance.data["item"].tags() - self.log.info(instance.data["tags"]) - return diff --git a/pype/plugins/nukestudio/publish/collect.py b/pype/plugins/nukestudio/publish/collect_clips.py similarity index 68% rename from pype/plugins/nukestudio/publish/collect.py rename to pype/plugins/nukestudio/publish/collect_clips.py index de6b2b3fca..69ec4814e9 100644 --- a/pype/plugins/nukestudio/publish/collect.py +++ b/pype/plugins/nukestudio/publish/collect_clips.py @@ -1,28 +1,14 @@ from pyblish import api -class CollectFramerate(api.ContextPlugin): - """Collect framerate from selected sequence.""" - order = api.CollectorOrder - label = "Collect Framerate" - hosts = ["nukestudio"] - - def process(self, context): - for item in context.data.get("selection", []): - context.data["framerate"] = item.sequence().framerate().toFloat() - return - - -class CollectTrackItems(api.ContextPlugin): +class CollectClips(api.ContextPlugin): """Collect all Track items selection.""" order = api.CollectorOrder - label = "Collect Track Items" + label = "Collect Clips" hosts = ["nukestudio"] def process(self, context): - import os - data = {} for item in context.data.get("selection", []): self.log.info("__ item: {}".format(item)) @@ -43,13 +29,13 @@ class CollectTrackItems(api.ContextPlugin): } for key, value in data.items(): - + family = "clip" context.create_instance( name=key, - subset="trackItem", + subset="{0}{1}".format(family, 'Default'), asset=value["item"].name(), item=value["item"], - family="trackItem", + family=family, tasks=value["tasks"], startFrame=value["startFrame"], endFrame=value["endFrame"], diff --git a/pype/plugins/nukestudio/publish/collect_framerate.py b/pype/plugins/nukestudio/publish/collect_framerate.py new file mode 100644 index 0000000000..822be8fb9b --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_framerate.py @@ -0,0 +1,13 @@ +from pyblish import api + +class CollectFramerate(api.ContextPlugin): + """Collect framerate from selected sequence.""" + + order = api.CollectorOrder + label = "Collect Framerate" + hosts = ["nukestudio"] + + def process(self, context): + for item in context.data.get("selection", []): + context.data["framerate"] = item.sequence().framerate().toFloat() + return diff --git a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py new file mode 100644 index 0000000000..b421d31f79 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py @@ -0,0 +1,72 @@ +import pyblish.api +from avalon import api + + +class CollectHierarchyContext(pyblish.api.ContextPlugin): + """Collecting hierarchy context from `parents` and `hierarchy` data + present in `clip` family instances coming from the request json data file + + It will add `hierarchical_context` into each instance for integrate + plugins to be able to create needed parents for the context if they + don't exist yet + """ + + label = "Collect Hierarchy Context" + order = pyblish.api.CollectorOrder + 0.1 + + def update_dict(self, ex_dict, new_dict): + for key in ex_dict: + if key in new_dict and isinstance(ex_dict[key], dict): + new_dict[key] = self.update_dict(ex_dict[key], new_dict[key]) + else: + new_dict[key] = ex_dict[key] + return new_dict + + def process(self, context): + json_data = context.data.get("jsonData", None) + temp_context = {} + for instance in json_data['instances']: + if instance['family'] in 'projectfile': + continue + + in_info = {} + name = instance['name'] + # suppose that all instances are Shots + in_info['entity_type'] = 'Shot' + + instance_pyblish = [ + i for i in context.data["instances"] if i.data['asset'] in name][0] + in_info['custom_attributes'] = { + 'fend': instance_pyblish.data['endFrame'], + 'fstart': instance_pyblish.data['startFrame'], + 'fps': instance_pyblish.data['fps'] + } + + in_info['tasks'] = instance['tasks'] + + parents = instance.get('parents', []) + + actual = {name: in_info} + + for parent in reversed(parents): + next_dict = {} + parent_name = parent["entityName"] + next_dict[parent_name] = {} + next_dict[parent_name]["entity_type"] = parent["entityType"] + next_dict[parent_name]["childs"] = actual + actual = next_dict + + temp_context = self.update_dict(temp_context, actual) + self.log.debug(temp_context) + + # TODO: 100% sure way of get project! Will be Name or Code? + project_name = api.Session["AVALON_PROJECT"] + final_context = {} + final_context[project_name] = {} + final_context[project_name]['entity_type'] = 'Project' + final_context[project_name]['childs'] = temp_context + + # adding hierarchy context to instance + context.data["hierarchyContext"] = final_context + self.log.debug("context.data[hierarchyContext] is: {}".format( + context.data["hierarchyContext"])) diff --git a/pype/plugins/nukestudio/publish/collect_metadata.py b/pype/plugins/nukestudio/publish/collect_metadata.py new file mode 100644 index 0000000000..23d36ba4a2 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_metadata.py @@ -0,0 +1,30 @@ +from pyblish import api + + +class CollectClipMetadata(api.InstancePlugin): + """Collect Metadata from selected track items.""" + + order = api.CollectorOrder + 0.01 + label = "Collect Metadata" + hosts = ["nukestudio"] + + def process(self, instance): + item = instance.data["item"] + ti_metadata = self.metadata_to_string(dict(item.metadata())) + ms_metadata = self.metadata_to_string( + dict(item.source().mediaSource().metadata())) + + instance.data["clipMetadata"] = ti_metadata + instance.data["mediaSourceMetadata"] = ms_metadata + + self.log.info(instance.data["clipMetadata"]) + self.log.info(instance.data["mediaSourceMetadata"]) + return + + def metadata_to_string(self, metadata): + data = dict() + for k, v in metadata.items(): + if v not in ["-", ""]: + data[str(k)] = v + + return data diff --git a/pype/plugins/nukestudio/publish/collect_subsets.py b/pype/plugins/nukestudio/publish/collect_subsets.py new file mode 100644 index 0000000000..b27a718f49 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_subsets.py @@ -0,0 +1,45 @@ +from pyblish import api + + +class CollectClipSubsets(api.InstancePlugin): + """Collect Subsets from selected Clips, Tags, Preset.""" + + order = api.CollectorOrder + 0.01 + label = "Collect Subsets" + hosts = ["nukestudio"] + families = ['clip'] + + def process(self, instance): + tags = instance.data.get('tags', None) + presets = instance.context.data['presets'][ + instance.context.data['host']] + if tags: + self.log.info(tags) + + if presets: + self.log.info(presets) + + # get presets and tags + # iterate tags and get task family + # iterate tags and get host family + # iterate tags and get handles family + + instance = instance.context.create_instance(instance_name) + + instance.data.update({ + "subset": subset_name, + "stagingDir": staging_dir, + "task": task, + "representation": ext[1:], + "host": host, + "asset": asset_name, + "label": label, + "name": name, + # "hierarchy": hierarchy, + # "parents": parents, + "family": family, + "families": [families, 'ftrack'], + "publish": True, + # "files": files_list + }) + instances.append(instance) diff --git a/pype/plugins/nukestudio/publish/collect_tags.py b/pype/plugins/nukestudio/publish/collect_tags.py new file mode 100644 index 0000000000..9ae34d415f --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_tags.py @@ -0,0 +1,30 @@ +from pyblish import api + + +class CollectClipTags(api.InstancePlugin): + """Collect Tags from selected track items.""" + + order = api.CollectorOrder + label = "Collect Tags" + hosts = ["nukestudio"] + families = ['clip'] + + def process(self, instance): + tags = instance.data["item"].tags() + + tags_d = [] + if tags: + for t in tags: + tag_data = { + "name": t.name(), + "object": t, + "metadata": t.metadata(), + "inTime": t.inTime(), + "outTime": t.outTime(), + } + tags_d.append(tag_data) + + instance.data["tags"] = tags_d + + self.log.info(instance.data["tags"]) + return diff --git a/pype/plugins/nukestudio/publish/integrate_assumed_destination.py b/pype/plugins/nukestudio/publish/integrate_assumed_destination.py new file mode 100644 index 0000000000..c1936994e4 --- /dev/null +++ b/pype/plugins/nukestudio/publish/integrate_assumed_destination.py @@ -0,0 +1,132 @@ +import pyblish.api +import os + +from avalon import io, api + + +class IntegrateAssumedDestination(pyblish.api.InstancePlugin): + """Generate the assumed destination path where the file will be stored""" + + label = "Integrate Assumed Destination" + order = pyblish.api.IntegratorOrder - 0.05 + families = ["clip", "projectfile"] + + def process(self, instance): + + self.create_destination_template(instance) + + template_data = instance.data["assumedTemplateData"] + # template = instance.data["template"] + + anatomy = instance.context.data['anatomy'] + # template = anatomy.publish.path + anatomy_filled = anatomy.format(template_data) + mock_template = anatomy_filled.publish.path + + # For now assume resources end up in a "resources" folder in the + # published folder + mock_destination = os.path.join(os.path.dirname(mock_template), + "resources") + + # Clean the path + mock_destination = os.path.abspath(os.path.normpath(mock_destination)) + + # Define resource destination and transfers + resources = instance.data.get("resources", list()) + transfers = instance.data.get("transfers", list()) + for resource in resources: + + # Add destination to the resource + source_filename = os.path.basename(resource["source"]) + destination = os.path.join(mock_destination, source_filename) + + # Force forward slashes to fix issue with software unable + # to work correctly with backslashes in specific scenarios + # (e.g. escape characters in PLN-151 V-Ray UDIM) + destination = destination.replace("\\", "/") + + resource['destination'] = destination + + # Collect transfers for the individual files of the resource + # e.g. all individual files of a cache or UDIM textures. + files = resource['files'] + for fsrc in files: + fname = os.path.basename(fsrc) + fdest = os.path.join(mock_destination, fname) + transfers.append([fsrc, fdest]) + + instance.data["resources"] = resources + instance.data["transfers"] = transfers + + def create_destination_template(self, instance): + """Create a filepath based on the current data available + + Example template: + {root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/ + {subset}.{representation} + Args: + instance: the instance to publish + + Returns: + file path (str) + """ + + # get all the stuff from the database + subset_name = instance.data["subset"] + self.log.info(subset_name) + asset_name = instance.data["asset"] + project_name = api.Session["AVALON_PROJECT"] + + project = io.find_one({"type": "project", + "name": project_name}, + projection={"config": True, "data": True}) + + template = project["config"]["template"]["publish"] + # anatomy = instance.context.data['anatomy'] + + asset = io.find_one({"type": "asset", + "name": asset_name, + "parent": project["_id"]}) + + assert asset, ("No asset found by the name '{}' " + "in project '{}'".format(asset_name, project_name)) + silo = asset['silo'] + + subset = io.find_one({"type": "subset", + "name": subset_name, + "parent": asset["_id"]}) + + # assume there is no version yet, we start at `1` + version = None + version_number = 1 + if subset is not None: + version = io.find_one({"type": "version", + "parent": subset["_id"]}, + sort=[("name", -1)]) + + # if there is a subset there ought to be version + if version is not None: + version_number += version["name"] + + if instance.data.get('version'): + version_number = int(instance.data.get('version')) + + hierarchy = asset['data']['parents'] + if hierarchy: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*hierarchy) + + template_data = {"root": api.Session["AVALON_PROJECTS"], + "project": {"name": project_name, + "code": project['data']['code']}, + "silo": silo, + "family": instance.data['family'], + "asset": asset_name, + "subset": subset_name, + "version": version_number, + "hierarchy": hierarchy, + "representation": "TEMP"} + + instance.data["assumedTemplateData"] = template_data + self.log.info(template_data) + instance.data["template"] = template diff --git a/pype/plugins/nukestudio/publish/integrate_ftrack_component_overwrite.py b/pype/plugins/nukestudio/publish/integrate_ftrack_component_overwrite.py new file mode 100644 index 0000000000..047fd8462c --- /dev/null +++ b/pype/plugins/nukestudio/publish/integrate_ftrack_component_overwrite.py @@ -0,0 +1,21 @@ +import pyblish.api + + +class IntegrateFtrackComponentOverwrite(pyblish.api.InstancePlugin): + """ + Set `component_overwrite` to True on all instances `ftrackComponentsList` + """ + + order = pyblish.api.IntegratorOrder + 0.49 + label = 'Overwrite ftrack created versions' + families = ["clip"] + optional = True + active = False + + def process(self, instance): + component_list = instance.data['ftrackComponentsList'] + + for cl in component_list: + cl['component_overwrite'] = True + self.log.debug('Component {} overwriting'.format( + cl['component_data']['name'])) diff --git a/pype/plugins/nukestudio/publish/integrate_hierarchy_avalon.py b/pype/plugins/nukestudio/publish/integrate_hierarchy_avalon.py new file mode 100644 index 0000000000..0f7fdb20d3 --- /dev/null +++ b/pype/plugins/nukestudio/publish/integrate_hierarchy_avalon.py @@ -0,0 +1,140 @@ +import pyblish.api +from avalon import io + + +class IntegrateHierarchyToAvalon(pyblish.api.ContextPlugin): + """ + Create entities in ftrack based on collected data from premiere + + """ + + order = pyblish.api.IntegratorOrder - 0.1 + label = 'Integrate Hierarchy To Avalon' + families = ['clip'] + + def process(self, context): + if "hierarchyContext" not in context.data: + return + + self.db = io + if not self.db.Session: + self.db.install() + + input_data = context.data["hierarchyContext"] + self.import_to_avalon(input_data) + + def import_to_avalon(self, input_data, parent=None): + + for name in input_data: + self.log.info('input_data[name]: {}'.format(input_data[name])) + entity_data = input_data[name] + entity_type = entity_data['entity_type'] + + data = {} + # Process project + if entity_type.lower() == 'project': + entity = self.db.find_one({'type': 'project'}) + # TODO: should be in validator? + assert (entity is not None), "Didn't find project in DB" + + # get data from already existing project + for key, value in entity.get('data', {}).items(): + data[key] = value + + self.av_project = entity + # Raise error if project or parent are not set + elif self.av_project is None or parent is None: + raise AssertionError( + "Collected items are not in right order!" + ) + # Else process assset + else: + entity = self.db.find_one({'type': 'asset', 'name': name}) + # Create entity if doesn't exist + if entity is None: + if self.av_project['_id'] == parent['_id']: + silo = None + elif parent['silo'] is None: + silo = parent['name'] + else: + silo = parent['silo'] + entity = self.create_avalon_asset(name, silo) + self.log.info('entity: {}'.format(entity)) + self.log.info('data: {}'.format(entity.get('data', {}))) + self.log.info('____1____') + data['entityType'] = entity_type + # TASKS + tasks = entity_data.get('tasks', []) + if tasks is not None or len(tasks) > 0: + data['tasks'] = tasks + parents = [] + visualParent = None + data = input_data[name] + if self.av_project['_id'] != parent['_id']: + visualParent = parent['_id'] + parents.extend(parent.get('data', {}).get('parents', [])) + parents.append(parent['name']) + data['visualParent'] = visualParent + data['parents'] = parents + + self.db.update_many( + {'_id': entity['_id']}, + {'$set': { + 'data': data, + }}) + + entity = self.db.find_one({'type': 'asset', 'name': name}) + self.log.info('entity: {}'.format(entity)) + self.log.info('data: {}'.format(entity.get('data', {}))) + self.log.info('____2____') + + # Else get data from already existing + else: + self.log.info('entity: {}'.format(entity)) + self.log.info('data: {}'.format(entity.get('data', {}))) + self.log.info('________') + for key, value in entity.get('data', {}).items(): + data[key] = value + + data['entityType'] = entity_type + # TASKS + tasks = entity_data.get('tasks', []) + if tasks is not None or len(tasks) > 0: + data['tasks'] = tasks + parents = [] + visualParent = None + # do not store project's id as visualParent (silo asset) + + if self.av_project['_id'] != parent['_id']: + visualParent = parent['_id'] + parents.extend(parent.get('data', {}).get('parents', [])) + parents.append(parent['name']) + data['visualParent'] = visualParent + data['parents'] = parents + + # CUSTOM ATTRIBUTES + for k, val in entity_data.get('custom_attributes', {}).items(): + data[k] = val + + # Update entity data with input data + self.db.update_many( + {'_id': entity['_id']}, + {'$set': { + 'data': data, + }}) + + if 'childs' in entity_data: + self.import_to_avalon(entity_data['childs'], entity) + + def create_avalon_asset(self, name, silo): + item = { + 'schema': 'avalon-core:asset-2.0', + 'name': name, + 'silo': silo, + 'parent': self.av_project['_id'], + 'type': 'asset', + 'data': {} + } + entity_id = self.db.insert_one(item).inserted_id + + return self.db.find_one({'_id': entity_id}) diff --git a/pype/plugins/nukestudio/publish/integrate_hierarchy_ftrack.py b/pype/plugins/nukestudio/publish/integrate_hierarchy_ftrack.py new file mode 100644 index 0000000000..d6d03e9722 --- /dev/null +++ b/pype/plugins/nukestudio/publish/integrate_hierarchy_ftrack.py @@ -0,0 +1,155 @@ +import pyblish.api + + +class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): + """ + Create entities in ftrack based on collected data from premiere + Example of entry data: + { + "ProjectXS": { + "entity_type": "Project", + "custom_attributes": { + "fps": 24,... + }, + "tasks": [ + "Compositing", + "Lighting",... *task must exist as task type in project schema* + ], + "childs": { + "sq01": { + "entity_type": "Sequence", + ... + } + } + } + } + """ + + order = pyblish.api.IntegratorOrder + label = 'Integrate Hierarchy To Ftrack' + families = ["clip"] + optional = False + + def process(self, context): + self.context = context + if "hierarchyContext" not in context.data: + return + + self.ft_project = None + self.session = context.data["ftrackSession"] + + input_data = context.data["hierarchyContext"] + + # adding ftrack types from presets + ftrack_types = context.data['ftrackTypes'] + + self.import_to_ftrack(input_data, ftrack_types) + + def import_to_ftrack(self, input_data, ftrack_types, parent=None): + for entity_name in input_data: + entity_data = input_data[entity_name] + entity_type = entity_data['entity_type'].capitalize() + + if entity_type.lower() == 'project': + query = 'Project where full_name is "{}"'.format(entity_name) + entity = self.session.query(query).one() + self.ft_project = entity + self.task_types = self.get_all_task_types(entity) + + elif self.ft_project is None or parent is None: + raise AssertionError( + "Collected items are not in right order!" + ) + + # try to find if entity already exists + else: + query = '{} where name is "{}" and parent_id is "{}"'.format( + entity_type, entity_name, parent['id'] + ) + try: + entity = self.session.query(query).one() + except Exception: + entity = None + + # Create entity if not exists + if entity is None: + entity = self.create_entity( + name=entity_name, + type=entity_type, + parent=parent + ) + # self.log.info('entity: {}'.format(dict(entity))) + # CUSTOM ATTRIBUTES + custom_attributes = entity_data.get('custom_attributes', []) + instances = [ + i for i in self.context.data["instances"] if i.data['asset'] in entity['name']] + for key in custom_attributes: + assert (key in entity['custom_attributes']), ( + 'Missing custom attribute') + + entity['custom_attributes'][key] = custom_attributes[key] + for instance in instances: + instance.data['ftrackShotId'] = entity['id'] + + self.session.commit() + + # TASKS + tasks = entity_data.get('tasks', []) + existing_tasks = [] + tasks_to_create = [] + for child in entity['children']: + if child.entity_type.lower() == 'task': + existing_tasks.append(child['name']) + # existing_tasks.append(child['type']['name']) + + for task in tasks: + if task in existing_tasks: + print("Task {} already exists".format(task)) + continue + tasks_to_create.append(task) + + for task in tasks_to_create: + self.create_task( + name=task, + task_type=ftrack_types[task], + parent=entity + ) + self.session.commit() + + if 'childs' in entity_data: + self.import_to_ftrack( + entity_data['childs'], ftrack_types, entity) + + def get_all_task_types(self, project): + tasks = {} + proj_template = project['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks + + def create_task(self, name, task_type, parent): + task = self.session.create('Task', { + 'name': name, + 'parent': parent + }) + # TODO not secured!!! - check if task_type exists + self.log.info(task_type) + self.log.info(self.task_types) + task['type'] = self.task_types[task_type] + + self.session.commit() + + return task + + def create_entity(self, name, type, parent): + entity = self.session.create(type, { + 'name': name, + 'parent': parent + }) + self.session.commit() + + return entity diff --git a/pype/plugins/nukestudio/publish/validate_names.py b/pype/plugins/nukestudio/publish/validate_names.py index 169febd764..52382e545d 100644 --- a/pype/plugins/nukestudio/publish/validate_names.py +++ b/pype/plugins/nukestudio/publish/validate_names.py @@ -10,7 +10,7 @@ class ValidateNames(api.InstancePlugin): """ order = api.ValidatorOrder - families = ["trackItem"] + families = ["clip"] match = api.Exact label = "Names" hosts = ["nukestudio"] @@ -39,4 +39,4 @@ class ValidateNamesFtrack(ValidateNames): """ order = api.ValidatorOrder - families = ["trackItem", "ftrack"] + families = ["clip", "ftrack"] diff --git a/pype/plugins/nukestudio/publish/validate_track_item.py b/pype/plugins/nukestudio/publish/validate_track_item.py index 3fe7a739ce..600bf58938 100644 --- a/pype/plugins/nukestudio/publish/validate_track_item.py +++ b/pype/plugins/nukestudio/publish/validate_track_item.py @@ -1,13 +1,13 @@ from pyblish import api -class ValidateTrackItem(api.InstancePlugin): +class ValidateClip(api.InstancePlugin): """Validate the track item to the sequence. Exact matching to optimize processing. """ order = api.ValidatorOrder - families = ["trackItem"] + families = ["clip"] match = api.Exact label = "Validate Track Item" hosts = ["nukestudio"] @@ -44,14 +44,3 @@ class ValidateTrackItem(api.InstancePlugin): assert sequence.framerate() == source_framerate, msg.format( "framerate", source_framerate, sequence.framerate() ) - -# -# class ValidateTrackItemFtrack(ValidateTrackItem): -# """Validate the track item to the sequence. -# -# Because we are matching the families exactly, we need this plugin to -# accommodate for the ftrack family addition. -# """ -# -# order = api.ValidatorOrder -# families = ["trackItem", "ftrack"] diff --git a/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox b/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox index 4045ea3335..128bde5456 100644 --- a/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox +++ b/setup/nukestudio/hiero_plugin_path/Templates/SharedTags.hrox @@ -1,19 +1,19 @@ - - + + - + 2 70 0 0 13 - + - - + + @@ -23,10 +23,10 @@ - + - - + + @@ -37,8 +37,8 @@ - - + + @@ -49,8 +49,8 @@ - - + + @@ -61,8 +61,8 @@ - - + + @@ -73,8 +73,8 @@ - - + + @@ -85,8 +85,8 @@ - - + + @@ -97,8 +97,8 @@ - - + + @@ -109,8 +109,8 @@ - - + + @@ -121,8 +121,8 @@ - - + + @@ -133,8 +133,8 @@ - - + + @@ -145,8 +145,8 @@ - - + + @@ -157,8 +157,8 @@ - - + + @@ -169,8 +169,8 @@ - - + + @@ -181,8 +181,8 @@ - - + + @@ -199,10 +199,10 @@ 0 0 - + - - + + @@ -214,8 +214,8 @@ - - + + @@ -227,8 +227,8 @@ - - + + @@ -240,8 +240,8 @@ - - + + @@ -253,8 +253,8 @@ - - + + @@ -268,8 +268,8 @@ - - + + @@ -283,8 +283,8 @@ - - + + @@ -298,8 +298,8 @@ - - + + @@ -313,8 +313,8 @@ - - + + @@ -328,8 +328,8 @@ - - + + @@ -343,8 +343,8 @@ - - + + @@ -358,8 +358,8 @@ - - + + @@ -373,8 +373,8 @@ - - + + @@ -392,47 +392,51 @@ 0 0 - + - - + + + - - + + + - - + + + - - + + + @@ -457,9 +461,9 @@ 0 0 2 - - - + + + From fccb376d9ce3720058ea9de037262126ac38c5bb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 May 2019 17:58:32 +0200 Subject: [PATCH 175/193] feat(nuke): incrementing script version only if not prerender only published --- .../nuke/_publish_unused/test_instances.py | 24 +++++++++++++++++++ .../nuke/publish/increment_script_version.py | 19 +++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 pype/plugins/nuke/_publish_unused/test_instances.py diff --git a/pype/plugins/nuke/_publish_unused/test_instances.py b/pype/plugins/nuke/_publish_unused/test_instances.py new file mode 100644 index 0000000000..e3fcc4b8f1 --- /dev/null +++ b/pype/plugins/nuke/_publish_unused/test_instances.py @@ -0,0 +1,24 @@ +import pyblish.api + + +class IncrementTestPlugin(pyblish.api.ContextPlugin): + """Increment current script version.""" + + order = pyblish.api.CollectorOrder + 0.5 + label = "Test Plugin" + hosts = ['nuke'] + + def process(self, context): + instances = context[:] + + prerender_check = list() + families_check = list() + for instance in instances: + if ("prerender" in str(instance)): + prerender_check.append(instance) + if instance.data.get("families", None): + families_check.append(True) + + if len(prerender_check) != len(families_check): + self.log.info(prerender_check) + self.log.info(families_check) diff --git a/pype/plugins/nuke/publish/increment_script_version.py b/pype/plugins/nuke/publish/increment_script_version.py index 77eab30a63..e8071ede93 100644 --- a/pype/plugins/nuke/publish/increment_script_version.py +++ b/pype/plugins/nuke/publish/increment_script_version.py @@ -16,7 +16,18 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): assert all(result["success"] for result in context.data["results"]), ( "Atomicity not held, aborting.") - from pype.lib import version_up - path = context.data["currentFile"] - nuke.scriptSaveAs(version_up(path)) - self.log.info('Incrementing script version') + instances = context[:] + + prerender_check = list() + families_check = list() + for instance in instances: + if ("prerender" in str(instance)): + prerender_check.append(instance) + if instance.data.get("families", None): + families_check.append(True) + + if len(prerender_check) != len(families_check): + from pype.lib import version_up + path = context.data["currentFile"] + nuke.scriptSaveAs(version_up(path)) + self.log.info('Incrementing script version') From 4e74bda9fa40e9aff64d13aa87462dbbcc939dae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:20:43 +0200 Subject: [PATCH 176/193] NOT_SELECTED moved into family widget --- pype/standalonepublish/app.py | 3 +-- pype/standalonepublish/widgets/widget_family.py | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 956cdb6300..5d3bfd0505 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -26,7 +26,6 @@ class Window(QtWidgets.QDialog): initialized = False WIDTH = 1100 HEIGHT = 500 - NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): super(Window, self).__init__(parent=parent) @@ -160,7 +159,7 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(asset['name']) else: self.valid_parent = False - self.widget_family.change_asset(self.NOT_SELECTED) + self.widget_family.change_asset(None) self.widget_family.on_data_changed() def keyPressEvent(self, event): diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 102705f98b..117fb30151 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -17,12 +17,14 @@ class FamilyWidget(QtWidgets.QWidget): data = dict() _jobs = dict() Separator = "---separator---" + NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent): super().__init__(parent) # Store internal states in here self.state = {"valid": False} self.parent_widget = parent + self.asset_name = self.NOT_SELECTED body = QtWidgets.QWidget() lists = QtWidgets.QWidget() @@ -132,7 +134,10 @@ class FamilyWidget(QtWidgets.QWidget): return self.parent_widget.db def change_asset(self, name): - self.input_asset.setText(name) + if name is None: + name = self.NOT_SELECTED + self.asset_name = name + self.on_data_changed() def _on_state_changed(self, state): self.state['valid'] = state From 941bf8e44c73d1dda18597a93e280f5805e0e2ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:23:18 +0200 Subject: [PATCH 177/193] input asset removed from widgets and subset result is filled even if asset is not selected --- .../widgets/widget_family.py | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 117fb30151..1029f71593 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -32,9 +32,7 @@ class FamilyWidget(QtWidgets.QWidget): container = QtWidgets.QWidget() list_families = QtWidgets.QListWidget() - input_asset = QtWidgets.QLineEdit() - input_asset.setEnabled(False) - input_asset.setStyleSheet("color: #BBBBBB;") + input_subset = QtWidgets.QLineEdit() input_result = QtWidgets.QLineEdit() input_result.setStyleSheet("color: #BBBBBB;") @@ -74,8 +72,6 @@ class FamilyWidget(QtWidgets.QWidget): layout.addWidget(QtWidgets.QLabel("Family")) layout.addWidget(list_families) - layout.addWidget(QtWidgets.QLabel("Asset")) - layout.addWidget(input_asset) layout.addWidget(QtWidgets.QLabel("Subset")) layout.addLayout(name_layout) layout.addWidget(input_result) @@ -93,6 +89,7 @@ class FamilyWidget(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(body) + layout.addWidget(lists) layout.addWidget(options, 0, QtCore.Qt.AlignLeft) layout.setContentsMargins(0, 0, 0, 0) @@ -101,7 +98,6 @@ class FamilyWidget(QtWidgets.QWidget): layout.addWidget(body) input_subset.textChanged.connect(self.on_data_changed) - input_asset.textChanged.connect(self.on_data_changed) list_families.currentItemChanged.connect(self.on_selection_changed) list_families.currentItemChanged.connect(header.set_item) version_checkbox.stateChanged.connect(self.on_version_refresh) @@ -112,7 +108,6 @@ class FamilyWidget(QtWidgets.QWidget): self.menu_subset = menu_subset self.btn_subset = btn_subset self.list_families = list_families - self.input_asset = input_asset self.input_result = input_result self.version_checkbox = version_checkbox self.version_spinbox = version_spinbox @@ -178,22 +173,37 @@ class FamilyWidget(QtWidgets.QWidget): self.input_subset.setText(action.text()) def _on_data_changed(self): - item = self.list_families.currentItem() + asset_name = self.asset_name subset_name = self.input_subset.text() - asset_name = self.input_asset.text() + item = self.list_families.currentItem() - # Get the assets from the database which match with the name - assets_db = self.db.find(filter={"type": "asset"}, projection={"name": 1}) - assets = [asset for asset in assets_db if asset_name in asset["name"]] if item is None: return - if assets: - # Get plugin and family - plugin = item.data(PluginRole) - if plugin is None: - return - family = plugin.family.rsplit(".", 1)[-1] + assets = None + if asset_name != self.NOT_SELECTED: + # Get the assets from the database which match with the name + assets_db = self.db.find( + filter={"type": "asset"}, + projection={"name": 1} + ) + assets = [ + asset for asset in assets_db if asset_name in asset["name"] + ] + + # Get plugin and family + plugin = item.data(PluginRole) + if plugin is None: + return + + family = plugin.family.rsplit(".", 1)[-1] + + # Update the result + if subset_name: + subset_name = subset_name[0].upper() + subset_name[1:] + self.input_result.setText("{}{}".format(family, subset_name)) + + if assets: # Get all subsets of the current asset asset_ids = [asset["_id"] for asset in assets] subsets = self.db.find(filter={"type": "subset", @@ -216,25 +226,20 @@ class FamilyWidget(QtWidgets.QWidget): self._build_menu(defaults) - # Update the result - if subset_name: - subset_name = subset_name[0].upper() + subset_name[1:] - self.input_result.setText("{}{}".format(family, subset_name)) - item.setData(ExistsRole, True) self.echo("Ready ..") else: self._build_menu([]) item.setData(ExistsRole, False) - if asset_name != self.parent_widget.NOT_SELECTED: + if asset_name != self.NOT_SELECTED: self.echo("'%s' not found .." % asset_name) self.on_version_refresh() # Update the valid state valid = ( + asset_name != self.NOT_SELECTED and subset_name.strip() != "" and - asset_name.strip() != "" and item.data(QtCore.Qt.ItemIsEnabled) and item.data(ExistsRole) ) @@ -246,12 +251,12 @@ class FamilyWidget(QtWidgets.QWidget): if not auto_version: return + asset_name = self.asset_name + subset_name = self.input_result.text() version = 1 - asset_name = self.input_asset.text() - subset_name = self.input_result.text() if ( - asset_name != self.parent_widget.NOT_SELECTED and + asset_name != self.NOT_SELECTED and subset_name.strip() != '' ): asset = self.db.find_one({ From 2604e9178c8f3342be49abd0a58268ec22e2f86d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:23:50 +0200 Subject: [PATCH 178/193] changed icon for tasks nodes --- .../widgets/model_tasks_template.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pype/standalonepublish/widgets/model_tasks_template.py b/pype/standalonepublish/widgets/model_tasks_template.py index 4af3b9eea7..bd1984029c 100644 --- a/pype/standalonepublish/widgets/model_tasks_template.py +++ b/pype/standalonepublish/widgets/model_tasks_template.py @@ -8,13 +8,13 @@ class TasksTemplateModel(TreeModel): COLUMNS = ["Tasks"] - def __init__(self): + def __init__(self, selectable=True): super(TasksTemplateModel, self).__init__() - self.selectable = False - self._icons = { - "__default__": awesome.icon("fa.folder-o", - color=style.colors.default) - } + self.selectable = selectable + self.icon = awesome.icon( + 'fa.calendar-check-o', + color=style.colors.default + ) def set_tasks(self, tasks): """Set assets to track by their database id @@ -32,13 +32,11 @@ class TasksTemplateModel(TreeModel): self.beginResetModel() - icon = self._icons["__default__"] for task in tasks: node = Node({ "Tasks": task, - "icon": icon + "icon": self.icon }) - self.add_child(node) self.endResetModel() From 4123cdfb165d74fa242f9929dd8b8af6decc2ad4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:26:09 +0200 Subject: [PATCH 179/193] added tasks view into asset widget --- .../standalonepublish/widgets/widget_asset.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 45e9757d71..e7d72a9db0 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -2,6 +2,8 @@ import contextlib from . import QtWidgets, QtCore from . import RecursiveSortFilterProxyModel, AssetModel, AssetView from . import awesome, style +from . import TasksTemplateModel, DeselectableTreeView + @contextlib.contextmanager def preserve_expanded_rows(tree_view, @@ -128,7 +130,7 @@ class AssetWidget(QtWidgets.QWidget): self.parent_widget = parent - layout = QtWidgets.QVBoxLayout(self) + layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) @@ -163,12 +165,29 @@ class AssetWidget(QtWidgets.QWidget): layout.addLayout(header) layout.addWidget(view) + # tasks + task_view = DeselectableTreeView() + task_view.setIndentation(0) + task_view.setHeaderHidden(True) + + task_model = TasksTemplateModel() + task_view.setModel(task_model) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(4) + main_layout.addLayout(layout, 80) + main_layout.addWidget(task_view, 20) + # Signals/Slots selection = view.selectionModel() selection.selectionChanged.connect(self.selection_changed) selection.currentChanged.connect(self.current_changed) refresh.clicked.connect(self.refresh) + + self.task_view = task_view + self.task_model = task_model self.refreshButton = refresh self.model = model self.proxy = proxy From 9f5d932e0a2eff60246e61791738a1a6d03f9e8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:26:33 +0200 Subject: [PATCH 180/193] tasks widget now loads tasks from selected asset --- pype/standalonepublish/widgets/widget_asset.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index e7d72a9db0..92a4d1d88f 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -185,6 +185,7 @@ class AssetWidget(QtWidgets.QWidget): selection.currentChanged.connect(self.current_changed) refresh.clicked.connect(self.refresh) + self.selection_changed.connect(self._refresh_tasks) self.task_view = task_view self.task_model = task_model @@ -242,6 +243,16 @@ class AssetWidget(QtWidgets.QWidget): def refresh(self): self._refresh_model() + def _refresh_tasks(self): + tasks = [] + selected = self.get_selected_assets() + if len(selected) == 1: + asset = self.db.find_one({ + "_id": selected[0], "type": "asset" + }) + tasks = asset['data'].get('tasks', []) + self.task_model.set_tasks(tasks) + def get_active_asset(self): """Return the asset id the current asset.""" current = self.view.currentIndex() From b4115f2867d9564f6074887ab2cdf8f738968916 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:27:29 +0200 Subject: [PATCH 181/193] getting tasks from asset is more secure --- pype/standalonepublish/widgets/widget_asset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 92a4d1d88f..4b27ba808e 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -250,7 +250,8 @@ class AssetWidget(QtWidgets.QWidget): asset = self.db.find_one({ "_id": selected[0], "type": "asset" }) - tasks = asset['data'].get('tasks', []) + if asset: + tasks = asset.get('data', {}).get('tasks', []) self.task_model.set_tasks(tasks) def get_active_asset(self): From 113bd52638b20e5211b07984109024fb5d962257 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:27:40 +0200 Subject: [PATCH 182/193] collect data also return task --- pype/standalonepublish/widgets/widget_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 4b27ba808e..41163e13d6 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -201,10 +201,17 @@ class AssetWidget(QtWidgets.QWidget): def collect_data(self): project = self.db.find_one({'type': 'project'}) asset = self.db.find_one({'_id': self.get_active_asset()}) + + try: + index = self.task_view.selectedIndexes()[0] + task = self.task_model.itemData(index)[0] + except Exception: + task = None data = { 'project': project['name'], 'asset': asset['name'], - 'parents': self.get_parents(asset) + 'parents': self.get_parents(asset), + 'task': task } return data From 72b8f24c85056745c589e55f45ba445f43cd5217 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 May 2019 18:28:06 +0200 Subject: [PATCH 183/193] fixed subset name in collect_data --- pype/plugins/standalonepublish/publish/collect_context.py | 2 +- pype/standalonepublish/widgets/widget_family.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index cbe9df1ef6..2f3ca1ca27 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -55,7 +55,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instance = context.create_instance(subset) instance.data.update({ - "subset": family + subset, + "subset": subset, "asset": asset_name, "label": family + subset, "name": family + subset, diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 1029f71593..78388d17d8 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -119,7 +119,7 @@ class FamilyWidget(QtWidgets.QWidget): family = plugin.family.rsplit(".", 1)[-1] data = { 'family': family, - 'subset': self.input_subset.text(), + 'subset': self.input_result.text(), 'version': self.version_spinbox.value() } return data From 65be5efb5c6137957222ae3c158314d806622479 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 May 2019 09:57:58 +0200 Subject: [PATCH 184/193] fix(nuke): increment script version when prerender desabled --- pype/plugins/nuke/publish/increment_script_version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/increment_script_version.py b/pype/plugins/nuke/publish/increment_script_version.py index e8071ede93..2e33e65528 100644 --- a/pype/plugins/nuke/publish/increment_script_version.py +++ b/pype/plugins/nuke/publish/increment_script_version.py @@ -21,11 +21,12 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): prerender_check = list() families_check = list() for instance in instances: - if ("prerender" in str(instance)): + if ("prerender" in str(instance)) and instance.data.get("families", None): prerender_check.append(instance) if instance.data.get("families", None): families_check.append(True) + if len(prerender_check) != len(families_check): from pype.lib import version_up path = context.data["currentFile"] From 346c824f720a35decb2237f0b6d489597f3b091e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 May 2019 10:43:12 +0200 Subject: [PATCH 185/193] feat(nuke): fixing handles for publishing writes --- pype/plugins/nuke/publish/collect_instances.py | 5 ++++- pype/plugins/nuke/publish/collect_writes.py | 3 +++ pype/plugins/nuke/publish/validate_script.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 8a2bb06fff..e9db556a9f 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -17,6 +17,10 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): def process(self, context): asset_data = io.find_one({"type": "asset", "name": api.Session["AVALON_ASSET"]}) + + # add handles into context + context.data['handles'] = int(asset_data["data"].get("handles", 0)) + self.log.debug("asset_data: {}".format(asset_data["data"])) instances = [] # creating instances per write node @@ -51,7 +55,6 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): "family": avalon_knob_data["family"], "avalonKnob": avalon_knob_data, "publish": node.knob('publish').value(), - "handles": int(asset_data["data"].get("handles", 0)), "step": 1, "fps": int(nuke.root()['fps'].value()) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index ce37774ac9..68cd227280 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -35,10 +35,12 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): output_type = "mov" # Get frame range + handles = instance.context.data.get('handles', 0) first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) if node["use_limit"].getValue(): + handles = 0 first_frame = int(node["first"].getValue()) last_frame = int(node["last"].getValue()) @@ -76,6 +78,7 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): "outputDir": output_dir, "ext": ext, "label": label, + "handles": handles, "startFrame": first_frame, "endFrame": last_frame, "outputType": output_type, diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index ad4a83b32f..08c91dab31 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -28,7 +28,7 @@ class ValidateScript(pyblish.api.InstancePlugin): ] # Value of these attributes can be found on parents - hierarchical_attributes = ["fps", "resolution_width", "resolution_height", "pixel_aspect"] + hierarchical_attributes = ["fps", "resolution_width", "resolution_height", "pixel_aspect", "handles"] missing_attributes = [] asset_attributes = {} From 39902e50a0e7abeb2b1a760b6b271bf113de1456 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 May 2019 17:51:54 +0200 Subject: [PATCH 186/193] remove echo label under assets --- pype/standalonepublish/app.py | 33 ++----------------- .../widgets/widget_family.py | 8 ++--- 2 files changed, 4 insertions(+), 37 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 5d3bfd0505..da5fbbba10 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -39,19 +39,9 @@ class Window(QtWidgets.QDialog): # Validators self.valid_parent = False - # statusbar - added under asset_widget - label_message = QtWidgets.QLabel() - label_message.setFixedHeight(20) - # assets widget - widget_assets_wrap = QtWidgets.QWidget() - widget_assets_wrap.setContentsMargins(0, 0, 0, 0) widget_assets = AssetWidget(self) - layout_assets = QtWidgets.QVBoxLayout(widget_assets_wrap) - layout_assets.addWidget(widget_assets) - layout_assets.addWidget(label_message) - # family widget widget_family = FamilyWidget(self) @@ -66,10 +56,10 @@ class Window(QtWidgets.QDialog): QtWidgets.QSizePolicy.Expanding ) body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(widget_assets_wrap) + body.addWidget(widget_assets) body.addWidget(widget_family) body.addWidget(widget_components) - body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) + body.setStretchFactor(body.indexOf(widget_assets), 2) body.setStretchFactor(body.indexOf(widget_family), 3) body.setStretchFactor(body.indexOf(widget_components), 5) @@ -81,13 +71,10 @@ class Window(QtWidgets.QDialog): # signals widget_assets.selection_changed.connect(self.on_asset_changed) - self.label_message = label_message self.widget_assets = widget_assets self.widget_family = widget_family self.widget_components = widget_components - self.echo("Connected to Database") - # on start self.on_start() @@ -130,22 +117,6 @@ class Window(QtWidgets.QDialog): parents.append(parent['name']) return parents - def echo(self, message): - ''' Shows message in label that disappear in 5s - :param message: Message that will be displayed - :type message: str - ''' - self.label_message.setText(str(message)) - def clear_text(): - ''' Helps prevent crash if this Window object - is deleted before 5s passed - ''' - try: - self.label_message.set_text("") - except: - pass - QtCore.QTimer.singleShot(5000, lambda: clear_text()) - def on_asset_changed(self): '''Callback on asset selection changed diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 78388d17d8..63776b1df3 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -227,12 +227,12 @@ class FamilyWidget(QtWidgets.QWidget): self._build_menu(defaults) item.setData(ExistsRole, True) - self.echo("Ready ..") else: self._build_menu([]) item.setData(ExistsRole, False) if asset_name != self.NOT_SELECTED: - self.echo("'%s' not found .." % asset_name) + # TODO add logging into standalone_publish + print("'%s' not found .." % asset_name) self.on_version_refresh() @@ -339,10 +339,6 @@ class FamilyWidget(QtWidgets.QWidget): self.list_families.setCurrentItem(self.list_families.item(0)) - def echo(self, message): - if hasattr(self.parent_widget, 'echo'): - self.parent_widget.echo(message) - def schedule(self, func, time, channel="default"): try: self._jobs[channel].stop() From 1f0836b433db7b0e675040297819fd42cb0e6b85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 May 2019 17:55:00 +0200 Subject: [PATCH 187/193] tasks widget is shown only if are any tasks available --- pype/standalonepublish/widgets/widget_asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 41163e13d6..54b7f7db44 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -169,6 +169,7 @@ class AssetWidget(QtWidgets.QWidget): task_view = DeselectableTreeView() task_view.setIndentation(0) task_view.setHeaderHidden(True) + task_view.setVisible(False) task_model = TasksTemplateModel() task_view.setModel(task_model) @@ -260,6 +261,7 @@ class AssetWidget(QtWidgets.QWidget): if asset: tasks = asset.get('data', {}).get('tasks', []) self.task_model.set_tasks(tasks) + self.task_view.setVisible(len(tasks)>0) def get_active_asset(self): """Return the asset id the current asset.""" From 0e48fa33993ae81a8bf2c68807cd3cdb34463c00 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 15 May 2019 16:59:04 +0100 Subject: [PATCH 188/193] fix collector to include the new data --- .../standalonepublish/publish/collect_context.py | 13 ++++++------- .../publish/integrate_ftrack_instances.py | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index cbe9df1ef6..6ac2dca936 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -55,10 +55,10 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instance = context.create_instance(subset) instance.data.update({ - "subset": family + subset, + "subset": subset, "asset": asset_name, - "label": family + subset, - "name": family + subset, + "label": subset, + "name": subset, "family": family, "families": [family, 'ftrack'], }) @@ -74,10 +74,9 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): collections, remainder = clique.assemble(component['files']) if collections: self.log.debug(collections) - range = collections[0].format('{range}') - instance.data['startFrame'] = range.split('-')[0] - instance.data['endFrame'] = range.split('-')[1] - + instance.data['startFrame'] = component['startFrame'] + instance.data['endFrame'] = component['endFrame'] + instance.data['frameRate'] = component['frameRate'] instance.data["files"].append(component) instance.data["representations"].append(component) diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py index 8d938bceb0..0dc9bb137c 100644 --- a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py @@ -57,19 +57,19 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "name": "thumbnail" # Default component name is "main". } elif comp['preview']: - if not instance.data.get('startFrameReview'): - instance.data['startFrameReview'] = instance.data['startFrame'] - if not instance.data.get('endFrameReview'): - instance.data['endFrameReview'] = instance.data['endFrame'] + if not comp.get('startFrameReview'): + comp['startFrameReview'] = comp['startFrame'] + if not comp.get('endFrameReview'): + comp['endFrameReview'] = instance.data['endFrame'] location = ft_session.query( 'Location where name is "ftrack.server"').one() component_data = { # Default component name is "main". "name": "ftrackreview-mp4", "metadata": {'ftr_meta': json.dumps({ - 'frameIn': int(instance.data['startFrameReview']), - 'frameOut': int(instance.data['endFrameReview']), - 'frameRate': 25.0})} + 'frameIn': int(comp['startFrameReview']), + 'frameOut': int(comp['endFrameReview']), + 'frameRate': float(comp['frameRate')]})} } else: component_data = { From 0c5f876137dbba24f2c7953419337108ecdf00a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 May 2019 21:45:52 +0200 Subject: [PATCH 189/193] feat(nuke): adding callback to nuke for change color of Loader nodes regarding to last version --- pype/nuke/lib.py | 47 +++++++++++++++++++++++++ pype/plugins/nuke/load/load_sequence.py | 8 +++-- setup/nuke/nuke_path/menu.py | 8 ++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 46b1d6e4c8..20e7dfb210 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -29,6 +29,53 @@ def onScriptLoad(): nuke.tcl('load movWriter') +def checkInventoryVersions(): + """ + Actiual version idetifier of Loaded containers + + Any time this function is run it will check all nodes and filter only Loader nodes for its version. It will get all versions from database + and check if the node is having actual version. If not then it will color it to red. + + """ + + + # get all Loader nodes by avalon attribute metadata + for each in nuke.allNodes(): + if each.Class() == 'Read': + container = avalon.nuke.parse_container(each) + + if container: + node = container["_tool"] + avalon_knob_data = get_avalon_knob_data(node) + + # get representation from io + representation = io.find_one({ + "type": "representation", + "_id": io.ObjectId(avalon_knob_data["representation"]) + }) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # check the available version and do match + # change color of node if not max verion + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + def writes_version_sync(): try: rootVersion = pype.get_version_from_path(nuke.root().name()) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index b4e3cfb8b5..f03e0fc97e 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -128,11 +128,15 @@ class LoadSequence(api.Loader): # add additional metadata from the version to imprint to Avalon knob add_keys = ["startFrame", "endFrame", "handles", - "source", "colorspace", "author", "fps"] + "source", "colorspace", "author", "fps", "version"] data_imprint = {} for k in add_keys: - data_imprint.update({k: context["version"]['data'][k]}) + if k is 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update({k: context["version"]['data'][k]}) + data_imprint.update({"objectName": read_name}) r["tile_color"].setValue(int("0x4ecd25ff", 16)) diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 9a96a52850..4982513b78 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -1,5 +1,10 @@ -from pype.nuke.lib import writes_version_sync, onScriptLoad +from pype.nuke.lib import ( + writes_version_sync, + onScriptLoad, + checkInventoryVersions +) + import nuke from pypeapp import Logger @@ -8,5 +13,6 @@ log = Logger().get_logger(__name__, "nuke") nuke.addOnScriptSave(writes_version_sync) nuke.addOnScriptSave(onScriptLoad) +nuke.addOnScriptSave(checkInventoryVersions) log.info('Automatic syncing of write file knob to script version') From d4dccba24884520fb44b6406d2e47c8f9a7fab8d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 May 2019 17:32:43 +0200 Subject: [PATCH 190/193] fix(nuke): logging cleaning --- pype/nuke/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index 376e8f95b8..a2b1aeda6e 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -25,8 +25,6 @@ from pypeapp import Logger log = Logger().get_logger(__name__, "nuke") -# log = api.Logger.getLogger(__name__, "nuke") - AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") PARENT_DIR = os.path.dirname(__file__) @@ -38,9 +36,8 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "nuke", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "nuke", "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nuke", "inventory") -self = sys.modules[__name__] -self.nLogger = None +# registering pyblish gui regarding settings in presets if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) @@ -66,6 +63,7 @@ class NukeHandler(logging.Handler): "fatal", "error" ]: + msg = self.format(record) nuke.message(msg) @@ -77,9 +75,6 @@ if nuke_handler.get_name() \ logging.getLogger().addHandler(nuke_handler) logging.getLogger().setLevel(logging.INFO) -if not self.nLogger: - self.nLogger = Logger - def reload_config(): """Attempt to reload pipeline at run-time. @@ -157,7 +152,7 @@ def uninstall(): def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node passthrough states on instance toggles.""" - self.log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( + log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) from avalon.nuke import ( From 16182d41eba4cd5859f7a6d645635a9473759346 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 May 2019 17:34:33 +0200 Subject: [PATCH 191/193] feat(nuke): Load nkscript as precomp with version --- pype/plugins/nuke/load/load_script_precomp.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 pype/plugins/nuke/load/load_script_precomp.py diff --git a/pype/plugins/nuke/load/load_script_precomp.py b/pype/plugins/nuke/load/load_script_precomp.py new file mode 100644 index 0000000000..6fd76edd03 --- /dev/null +++ b/pype/plugins/nuke/load/load_script_precomp.py @@ -0,0 +1,170 @@ +from avalon import api, style, io +from pype.nuke.lib import get_avalon_knob_data +import nuke +import os +from pype.api import Logger +log = Logger().get_logger(__name__, "nuke") + + + +class LinkAsGroup(api.Loader): + """Copy the published file to be pasted at the desired location""" + + representations = ["nk"] + families = ["*"] + + label = "Load Precomp" + order = 10 + icon = "file" + color = style.colors.dark + + def load(self, context, name, namespace, data): + + from avalon.nuke import containerise + # for k, v in context.items(): + # log.info("key: `{}`, value: {}\n".format(k, v)) + version = context['version'] + version_data = version.get("data", {}) + + vname = version.get("name", None) + first = version_data.get("startFrame", None) + last = version_data.get("endFrame", None) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + file = self.fname.replace("\\", "/") + self.log.info("file: {}\n".format(self.fname)) + + precomp_name = context["representation"]["context"]["subset"] + + # Set global in point to start frame (if in version.data) + start = context["version"]["data"].get("startFrame", None) + + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["startFrame", "endFrame", "handles", + "source", "author", "fps"] + + data_imprint = { + "start_frame": start, + "fstart": first, + "fend": last, + "version": vname + } + for k in add_keys: + data_imprint.update({k: context["version"]['data'][k]}) + data_imprint.update({"objectName": precomp_name}) + + # group context is set to precomp, so back up one level. + nuke.endGroup() + + # P = nuke.nodes.LiveGroup("file {}".format(file)) + P = nuke.createNode( + "Precomp", + "file {}".format(file)) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace", None) + self.log.info("colorspace: {}\n".format(colorspace)) + + + # ['version', 'file', 'reading', 'output', 'useOutput'] + + P["name"].setValue("{}_{}".format(name, namespace)) + P["useOutput"].setValue(True) + + with P: + # iterate trough all nodes in group node and find pype writes + writes = [n.name() for n in nuke.allNodes() + if n.Class() == "Write" + if get_avalon_knob_data(n)] + + # create panel for selecting output + panel_choices = " ".join(writes) + panel_label = "Select write node for output" + p = nuke.Panel("Select Write Node") + p.addEnumerationPulldown( + panel_label, panel_choices) + p.show() + P["output"].setValue(p.value(panel_label)) + + P["tile_color"].setValue(0xff0ff0ff) + + return containerise( + node=P, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + + node = nuke.toNode(container['objectName']) + + root = api.get_representation_path(representation).replace("\\","/") + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "endFrame": version["data"].get("endFrame"), + "version": version.get("name"), + "colorspace": version["data"].get("colorspace"), + "source": version["data"].get("source"), + "handles": version["data"].get("handles"), + "fps": version["data"].get("fps"), + "author": version["data"].get("author"), + "outputDir": version["data"].get("outputDir"), + }) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + + node["file"].setValue(root) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0xff0ff0ff", 16)) + + log.info("udated to version: {}".format(version.get("name"))) + + + def remove(self, container): + from avalon.nuke import viewer_update_and_undo_stop + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) From 4f4a45c54b201cd69ec056622eeeee351a94043e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 17 May 2019 11:21:43 +0200 Subject: [PATCH 192/193] fix(premiere): removing message_window from pype.api and adding link to the message into premiere --- pype/api.py | 5 ----- pype/premiere/__init__.py | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pype/api.py b/pype/api.py index fcdcbce82b..0acb80e383 100644 --- a/pype/api.py +++ b/pype/api.py @@ -46,8 +46,6 @@ from .lib import ( get_data_hierarchical_attr ) -from .widgets.message_window import message - __all__ = [ # plugin classes "Extractor", @@ -89,7 +87,4 @@ __all__ = [ "Colorspace", "Dataflow", - # QtWidgets - "message" - ] diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 74ce106de2..cc5abe115e 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -7,6 +7,8 @@ from pyblish import api as pyblish from pypeapp import Logger from .. import api +from ..widgets.message_window import message + import requests log = Logger().get_logger(__name__, "premiere") @@ -42,7 +44,7 @@ def request_aport(url_path, data={}): return req except Exception as e: - api.message(title="Premiere Aport Server", + message(title="Premiere Aport Server", message="Before you can run Premiere, start Aport Server. \n Error: {}".format( e), level="critical") @@ -99,7 +101,7 @@ def install(): # synchronize extensions extensions_sync() - api.message(title="pyblish_paths", message=str(reg_paths), level="info") + message(title="pyblish_paths", message=str(reg_paths), level="info") def uninstall(): From 8c2db60b26074f66578b8f91eec0651bfa2a7992 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 May 2019 16:21:47 +0200 Subject: [PATCH 193/193] (hotfix)/ added stagingDir to standalone publisher --- .../widgets/widget_component_item.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 43aa54a955..a58a292ec5 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -284,11 +284,19 @@ class ComponentItem(QtWidgets.QFrame): self.preview.change_checked(hover) def collect_data(self): + in_files = self.in_data['files'] + staging_dir = os.path.dirname(in_files[0]) + + files = [os.path.basename(file) for file in in_files] + if len(files) == 1: + files = files[0] + data = { 'ext': self.in_data['ext'], 'label': self.name.text(), - 'representation': self.input_repre.text(), - 'files': self.in_data['files'], + 'name': self.input_repre.text(), + 'stagingDir': staging_dir, + 'files': files, 'thumbnail': self.is_thumbnail(), 'preview': self.is_preview() }