diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 5ca2a42510..653f97b3dd 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -6,6 +6,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): """Add last workfile path to launch arguments. This is not possible to do for all applications the same way. + Checks 'start_last_workfile', if set to False, it will not open last + workfile. This property is set explicitly in Launcher. """ # Execute after workfile template copy diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index 6b08cdb444..bae967e25f 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -43,6 +43,7 @@ class GlobalHostDataHook(PreLaunchHook): "env": self.launch_context.env, + "start_last_workfile": self.data.get("start_last_workfile"), "last_workfile_path": self.data.get("last_workfile_path"), "log": self.log diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index 29e40d28c8..dd193616e6 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -40,7 +40,10 @@ class NonPythonHostHook(PreLaunchHook): ) # Add workfile path if exists workfile_path = self.data["last_workfile_path"] - if os.path.exists(workfile_path): + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path)): new_launch_args.append(workfile_path) # Append as whole list as these areguments should not be separated diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 0e1f44391e..a704c3ae68 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1490,6 +1490,7 @@ def _prepare_last_workfile(data, workdir): import avalon.api log = data["log"] + _workdir_data = data.get("workdir_data") if not _workdir_data: log.info( @@ -1503,9 +1504,15 @@ def _prepare_last_workfile(data, workdir): project_name = data["project_name"] task_name = data["task_name"] task_type = data["task_type"] - start_last_workfile = should_start_last_workfile( - project_name, app.host_name, task_name, task_type - ) + + start_last_workfile = data.get("start_last_workfile") + if start_last_workfile is None: + start_last_workfile = should_start_last_workfile( + project_name, app.host_name, task_name, task_type + ) + else: + log.info("Opening of last workfile was disabled by user") + data["start_last_workfile"] = start_last_workfile workfile_startup = should_workfile_tool_start( diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 4d86970f9c..fbaef05261 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -62,6 +62,7 @@ class ApplicationAction(api.Action): icon = None color = None order = 0 + data = {} _log = None required_session_keys = ( @@ -103,7 +104,8 @@ class ApplicationAction(api.Action): self.application.launch( project_name=project_name, asset_name=asset_name, - task_name=task_name + task_name=task_name, + **self.data ) except ApplictionExecutableNotFound as exc: diff --git a/openpype/tools/launcher/constants.py b/openpype/tools/launcher/constants.py index 7f394cb5ac..61f631759b 100644 --- a/openpype/tools/launcher/constants.py +++ b/openpype/tools/launcher/constants.py @@ -7,6 +7,8 @@ VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 ACTION_ID_ROLE = QtCore.Qt.UserRole + 3 ANIMATION_START_ROLE = QtCore.Qt.UserRole + 4 ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5 +FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 6 +ACTION_TOOLTIP_ROLE = QtCore.Qt.UserRole + 7 # Animation length in seconds ANIMATION_LEN = 7 diff --git a/openpype/tools/launcher/delegates.py b/openpype/tools/launcher/delegates.py index cef0f5e1a2..7b53658727 100644 --- a/openpype/tools/launcher/delegates.py +++ b/openpype/tools/launcher/delegates.py @@ -2,7 +2,8 @@ import time from Qt import QtCore, QtWidgets, QtGui from .constants import ( ANIMATION_START_ROLE, - ANIMATION_STATE_ROLE + ANIMATION_STATE_ROLE, + FORCE_NOT_OPEN_WORKFILE_ROLE ) @@ -69,6 +70,16 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): self._draw_animation(painter, option, index) super(ActionDelegate, self).paint(painter, option, index) + + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + rect = QtCore.QRectF(option.rect.x(), option.rect.height(), + 5, 5) + painter.setPen(QtCore.Qt.transparent) + painter.setBrush(QtGui.QColor(200, 0, 0)) + painter.drawEllipse(rect) + + painter.setBrush(self.extender_bg_brush) + is_group = False for group_role in self.group_roles: is_group = index.data(group_role) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 42ad509854..6ade9d33ed 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -2,19 +2,21 @@ import uuid import copy import logging import collections +import appdirs from . import lib from .constants import ( ACTION_ROLE, GROUP_ROLE, VARIANT_GROUP_ROLE, - ACTION_ID_ROLE + ACTION_ID_ROLE, + FORCE_NOT_OPEN_WORKFILE_ROLE ) from .actions import ApplicationAction from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import style, api -from openpype.lib import ApplicationManager +from openpype.lib import ApplicationManager, JSONSettingRegistry log = logging.getLogger(__name__) @@ -30,6 +32,13 @@ class ActionModel(QtGui.QStandardItemModel): # Cache of available actions self._registered_actions = list() self.items_by_id = {} + path = appdirs.user_data_dir("openpype", "pypeclub") + self.launcher_registry = JSONSettingRegistry("launcher", path) + + try: + _ = self.launcher_registry.get_item("force_not_open_workfile") + except ValueError: + self.launcher_registry.set_item("force_not_open_workfile", []) def discover(self): """Set up Actions cache. Run this for each new project.""" @@ -75,7 +84,8 @@ class ActionModel(QtGui.QStandardItemModel): "group": None, "icon": app.icon, "color": getattr(app, "color", None), - "order": getattr(app, "order", None) or 0 + "order": getattr(app, "order", None) or 0, + "data": {} } ) @@ -179,11 +189,17 @@ class ActionModel(QtGui.QStandardItemModel): self.beginResetModel() + stored = self.launcher_registry.get_item("force_not_open_workfile") items = [] for order in sorted(items_by_order.keys()): for item in items_by_order[order]: item_id = str(uuid.uuid4()) item.setData(item_id, ACTION_ID_ROLE) + + if self.is_force_not_open_workfile(item, + stored): + self.change_action_item(item, True) + self.items_by_id[item_id] = item items.append(item) @@ -222,6 +238,90 @@ class ActionModel(QtGui.QStandardItemModel): key=lambda action: (action.order, action.name) ) + def update_force_not_open_workfile_settings(self, is_checked, action_id): + """Store/remove config for forcing to skip opening last workfile. + + Args: + is_checked (bool): True to add, False to remove + action_id (str) + """ + action_item = self.items_by_id.get(action_id) + if not action_item: + return + + action = action_item.data(ACTION_ROLE) + actual_data = self._prepare_compare_data(action) + + stored = self.launcher_registry.get_item("force_not_open_workfile") + if is_checked: + stored.append(actual_data) + else: + final_values = [] + for config in stored: + if config != actual_data: + final_values.append(config) + stored = final_values + + self.launcher_registry.set_item("force_not_open_workfile", stored) + self.launcher_registry._get_item.cache_clear() + self.change_action_item(action_item, is_checked) + + def change_action_item(self, item, checked): + """Modifies tooltip and sets if opening of last workfile forbidden""" + tooltip = item.data(QtCore.Qt.ToolTipRole) + if checked: + tooltip += " (Not opening last workfile)" + + item.setData(tooltip, QtCore.Qt.ToolTipRole) + item.setData(checked, FORCE_NOT_OPEN_WORKFILE_ROLE) + + def is_application_action(self, action): + """Checks if item is of a ApplicationAction type + + Args: + action (action) + """ + if isinstance(action, list) and action: + action = action[0] + + return ApplicationAction in action.__bases__ + + def is_force_not_open_workfile(self, item, stored): + """Checks if application for task is marked to not open workfile + + There might be specific tasks where is unwanted to open workfile right + always (broken file, low performance). This allows artist to mark to + skip opening for combination (project, asset, task_name, app) + + Args: + item (QStandardItem) + stored (list) of dict + """ + action = item.data(ACTION_ROLE) + if not self.is_application_action(action): + return False + + actual_data = self._prepare_compare_data(action) + for config in stored: + if config == actual_data: + return True + + return False + + def _prepare_compare_data(self, action): + if isinstance(action, list) and action: + action = action[0] + + compare_data = {} + if action: + compare_data = { + "app_label": action.label.lower(), + "project_name": self.dbcon.Session["AVALON_PROJECT"], + "asset": self.dbcon.Session["AVALON_ASSET"], + "task_name": self.dbcon.Session["AVALON_TASK"] + } + return compare_data + class ProjectModel(QtGui.QStandardItemModel): """List of projects""" diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 55232c4d1d..ba0d9dd6b5 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -15,7 +15,8 @@ from .constants import ( ACTION_ID_ROLE, ANIMATION_START_ROLE, ANIMATION_STATE_ROLE, - ANIMATION_LEN + ANIMATION_LEN, + FORCE_NOT_OPEN_WORKFILE_ROLE ) @@ -96,6 +97,7 @@ class ActionBar(QtWidgets.QWidget): view.setViewMode(QtWidgets.QListView.IconMode) view.setResizeMode(QtWidgets.QListView.Adjust) view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setEditTriggers(QtWidgets.QListView.NoEditTriggers) view.setWrapping(True) view.setGridSize(QtCore.QSize(70, 75)) @@ -135,8 +137,16 @@ class ActionBar(QtWidgets.QWidget): project_handler.projects_refreshed.connect(self._on_projects_refresh) view.clicked.connect(self.on_clicked) + view.customContextMenuRequested.connect(self.on_context_menu) + + self._context_menu = None + self._discover_on_menu = False def discover_actions(self): + if self._context_menu is not None: + self._discover_on_menu = True + return + if self._animation_timer.isActive(): self._animation_timer.stop() self.model.discover() @@ -181,6 +191,46 @@ class ActionBar(QtWidgets.QWidget): self._animated_items.add(action_id) self._animation_timer.start() + def on_context_menu(self, point): + """Creates menu to force skip opening last workfile.""" + index = self.view.indexAt(point) + if not index.isValid(): + return + + action_item = index.data(ACTION_ROLE) + if not self.model.is_application_action(action_item): + return + + menu = QtWidgets.QMenu(self.view) + checkbox = QtWidgets.QCheckBox("Skip opening last workfile.", + menu) + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + checkbox.setChecked(True) + + action_id = index.data(ACTION_ID_ROLE) + checkbox.stateChanged.connect( + lambda: self.on_checkbox_changed(checkbox.isChecked(), + action_id)) + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + menu.addAction(action) + + self._context_menu = menu + global_point = self.mapToGlobal(point) + menu.exec_(global_point) + self._context_menu = None + if self._discover_on_menu: + self._discover_on_menu = False + self.discover_actions() + + def on_checkbox_changed(self, is_checked, action_id): + self.model.update_force_not_open_workfile_settings(is_checked, + action_id) + self.view.update() + if self._context_menu is not None: + self._context_menu.close() + def on_clicked(self, index): if not index or not index.isValid(): return @@ -189,6 +239,10 @@ class ActionBar(QtWidgets.QWidget): is_variant_group = index.data(VARIANT_GROUP_ROLE) if not is_group and not is_variant_group: action = index.data(ACTION_ROLE) + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + action.data["start_last_workfile"] = False + else: + action.data.pop("start_last_workfile", None) self._start_animation(index) self.action_clicked.emit(action) return