mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into settings_search
This commit is contained in:
commit
b1cc2b2da4
548 changed files with 27631 additions and 391079 deletions
|
|
@ -15,8 +15,12 @@ from .constants import (
|
|||
from .actions import ApplicationAction
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import style, api
|
||||
from openpype.lib import ApplicationManager, JSONSettingRegistry
|
||||
from avalon import api
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
from openpype.lib.applications import (
|
||||
CUSTOM_LAUNCH_APP_GROUPS,
|
||||
ApplicationManager
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -72,6 +76,9 @@ class ActionModel(QtGui.QStandardItemModel):
|
|||
if not app or not app.enabled:
|
||||
continue
|
||||
|
||||
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
|
||||
continue
|
||||
|
||||
# Get from app definition, if not there from app in project
|
||||
action = type(
|
||||
"app_{}".format(app_name),
|
||||
|
|
@ -313,7 +320,7 @@ class ActionModel(QtGui.QStandardItemModel):
|
|||
action = action[0]
|
||||
|
||||
compare_data = {}
|
||||
if action:
|
||||
if action and action.label:
|
||||
compare_data = {
|
||||
"app_label": action.label.lower(),
|
||||
"project_name": self.dbcon.Session["AVALON_PROJECT"],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from avalon.vendor import qtawesome
|
|||
|
||||
from .delegates import ActionDelegate
|
||||
from . import lib
|
||||
from .actions import ApplicationAction
|
||||
from .models import ActionModel
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from .constants import (
|
||||
|
|
@ -239,10 +240,12 @@ 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)
|
||||
# Change data of application action
|
||||
if issubclass(action, ApplicationAction):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -396,9 +396,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
|
|||
self._versionschanged()
|
||||
return
|
||||
|
||||
selected_subsets = self._subsets_widget.selected_subsets(
|
||||
_merged=True, _other=False
|
||||
)
|
||||
selected_subsets = self._subsets_widget.get_selected_merge_items()
|
||||
|
||||
asset_colors = {}
|
||||
asset_ids = []
|
||||
|
|
@ -423,35 +421,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
|
|||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
selection = self._subsets_widget.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
version_docs = None
|
||||
items = self._subsets_widget.get_selected_subsets()
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active and active in rows:
|
||||
item = active.data(self._subsets_widget.model.ItemRole)
|
||||
if (
|
||||
item is not None
|
||||
and not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
|
||||
if rows:
|
||||
version_docs = []
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(self._subsets_widget.model.ItemRole)
|
||||
if (
|
||||
item is None
|
||||
or item.get("isGroup")
|
||||
or item.get("isMerged")
|
||||
):
|
||||
continue
|
||||
version_docs.append(item["version_document"])
|
||||
version_docs = []
|
||||
for item in items:
|
||||
doc = item["version_document"]
|
||||
version_docs.append(doc)
|
||||
if version_doc is None:
|
||||
version_doc = doc
|
||||
|
||||
self._version_info_widget.set_version(version_doc)
|
||||
|
||||
|
|
|
|||
|
|
@ -287,9 +287,7 @@ class LoaderWindow(QtWidgets.QDialog):
|
|||
on selection change so they match current selection.
|
||||
"""
|
||||
# TODO do not touch inner attributes of asset widget
|
||||
last_asset_ids = self.data["state"]["assetIds"]
|
||||
if last_asset_ids:
|
||||
self._assets_widget.clear_underlines()
|
||||
self._assets_widget.clear_underlines()
|
||||
|
||||
def _assetschanged(self):
|
||||
"""Selected assets have changed"""
|
||||
|
|
@ -328,12 +326,11 @@ class LoaderWindow(QtWidgets.QDialog):
|
|||
asset_ids = self.data["state"]["assetIds"]
|
||||
# Skip setting colors if not asset multiselection
|
||||
if not asset_ids or len(asset_ids) < 2:
|
||||
self.clear_assets_underlines()
|
||||
self._versionschanged()
|
||||
return
|
||||
|
||||
selected_subsets = self._subsets_widget.selected_subsets(
|
||||
_merged=True, _other=False
|
||||
)
|
||||
selected_subsets = self._subsets_widget.get_selected_merge_items()
|
||||
|
||||
asset_colors = {}
|
||||
asset_ids = []
|
||||
|
|
@ -358,37 +355,16 @@ class LoaderWindow(QtWidgets.QDialog):
|
|||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
subsets = self._subsets_widget
|
||||
selection = subsets.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
items = self._subsets_widget.get_selected_subsets()
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active:
|
||||
if active in rows:
|
||||
item = active.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is not None and
|
||||
not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
self._version_info_widget.set_version(version_doc)
|
||||
|
||||
version_docs = []
|
||||
if rows:
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(subsets.model.ItemRole)
|
||||
if item is None:
|
||||
continue
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
for child in item.children():
|
||||
version_docs.append(child["version_document"])
|
||||
else:
|
||||
version_docs.append(item["version_document"])
|
||||
for item in items:
|
||||
doc = item["version_document"]
|
||||
version_docs.append(doc)
|
||||
if version_doc is None:
|
||||
version_doc = doc
|
||||
|
||||
self._version_info_widget.set_version(version_doc)
|
||||
|
||||
thumbnail_src_ids = [
|
||||
version_doc["_id"]
|
||||
|
|
@ -480,18 +456,7 @@ class LoaderWindow(QtWidgets.QDialog):
|
|||
self.echo("Grouping not enabled.")
|
||||
return
|
||||
|
||||
selected = []
|
||||
merged_items = []
|
||||
for item in subsets.selected_subsets(_merged=True):
|
||||
if item.get("isMerged"):
|
||||
merged_items.append(item)
|
||||
else:
|
||||
selected.append(item)
|
||||
|
||||
for merged_item in merged_items:
|
||||
for child_item in merged_item.children():
|
||||
selected.append(child_item)
|
||||
|
||||
selected = self._subsets_widget.get_selected_subsets()
|
||||
if not selected:
|
||||
self.echo("No selected subset.")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -18,26 +18,6 @@ def change_visibility(model, view, column_name, visible):
|
|||
view.setColumnHidden(index, not visible)
|
||||
|
||||
|
||||
def get_selected_items(rows, item_role):
|
||||
items = []
|
||||
for row_index in rows:
|
||||
item = row_index.data(item_role)
|
||||
if item.get("isGroup"):
|
||||
continue
|
||||
|
||||
elif item.get("isMerged"):
|
||||
for idx in range(row_index.model().rowCount(row_index)):
|
||||
child_index = row_index.child(idx, 0)
|
||||
item = child_index.data(item_role)
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
|
||||
else:
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def get_options(action, loader, parent, repre_contexts):
|
||||
"""Provides dialog to select value from loader provided options.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import re
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
||||
from avalon import (
|
||||
style,
|
||||
|
|
@ -22,6 +23,8 @@ from openpype.tools.utils.constants import (
|
|||
REMOTE_AVAILABILITY_ROLE
|
||||
)
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 90
|
||||
|
||||
|
||||
def is_filtering_recursible():
|
||||
"""Does Qt binding support recursive filtering for QSortFilterProxyModel?
|
||||
|
|
@ -179,6 +182,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
self._icons = {
|
||||
"subset": qtawesome.icon("fa.file-o", color=style.colors.default)
|
||||
}
|
||||
self._items_by_id = {}
|
||||
|
||||
self._doc_fetching_thread = None
|
||||
self._doc_fetching_stop = False
|
||||
|
|
@ -188,6 +192,15 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
|
||||
self.refresh()
|
||||
|
||||
def get_item_by_id(self, item_id):
|
||||
return self._items_by_id.get(item_id)
|
||||
|
||||
def add_child(self, new_item, *args, **kwargs):
|
||||
item_id = str(uuid4())
|
||||
new_item["id"] = item_id
|
||||
self._items_by_id[item_id] = new_item
|
||||
super(SubsetsModel, self).add_child(new_item, *args, **kwargs)
|
||||
|
||||
def set_assets(self, asset_ids):
|
||||
self._asset_ids = asset_ids
|
||||
self.refresh()
|
||||
|
|
@ -486,7 +499,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
def refresh(self):
|
||||
self.stop_fetch_thread()
|
||||
self.clear()
|
||||
|
||||
self._items_by_id = {}
|
||||
self.reset_sync_server()
|
||||
|
||||
if not self._asset_ids:
|
||||
|
|
@ -497,6 +510,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
|
||||
def on_doc_fetched(self):
|
||||
self.clear()
|
||||
self._items_by_id = {}
|
||||
self.beginResetModel()
|
||||
|
||||
asset_docs_by_id = self._doc_payload.get(
|
||||
|
|
@ -524,9 +538,13 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
return
|
||||
|
||||
self._fill_subset_items(
|
||||
asset_docs_by_id, subset_docs_by_id, last_versions_by_subset_id,
|
||||
asset_docs_by_id,
|
||||
subset_docs_by_id,
|
||||
last_versions_by_subset_id,
|
||||
repre_info_by_version_id
|
||||
)
|
||||
self.endResetModel()
|
||||
self.refreshed.emit(True)
|
||||
|
||||
def create_multiasset_group(
|
||||
self, subset_name, asset_ids, subset_counter, parent_item=None
|
||||
|
|
@ -538,7 +556,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
merge_group.update({
|
||||
"subset": "{} ({})".format(subset_name, len(asset_ids)),
|
||||
"isMerged": True,
|
||||
"childRow": 0,
|
||||
"subsetColor": subset_color,
|
||||
"assetIds": list(asset_ids),
|
||||
"icon": qtawesome.icon(
|
||||
|
|
@ -547,7 +564,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
)
|
||||
})
|
||||
|
||||
subset_counter += 1
|
||||
self.add_child(merge_group, parent_item)
|
||||
|
||||
return merge_group
|
||||
|
|
@ -567,8 +583,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
group_item = Item()
|
||||
group_item.update({
|
||||
"subset": group_name,
|
||||
"isGroup": True,
|
||||
"childRow": 0
|
||||
"isGroup": True
|
||||
})
|
||||
group_item.update(group_data)
|
||||
|
||||
|
|
@ -666,14 +681,14 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
index = self.index(item.row(), 0, parent_index)
|
||||
self.set_version(index, last_version)
|
||||
|
||||
self.endResetModel()
|
||||
self.refreshed.emit(True)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item = index.internalPointer()
|
||||
if role == ITEM_ID_ROLE:
|
||||
return item["id"]
|
||||
|
||||
if role == self.SortDescendingRole:
|
||||
if item.get("isGroup"):
|
||||
# Ensure groups be on top when sorting by descending order
|
||||
|
|
@ -1053,6 +1068,7 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
self._icons = lib.get_repre_icons()
|
||||
self._icons["repre"] = qtawesome.icon("fa.file-o",
|
||||
color=style.colors.default)
|
||||
self._items_by_id = {}
|
||||
|
||||
def set_version_ids(self, version_ids):
|
||||
self.version_ids = version_ids
|
||||
|
|
@ -1061,6 +1077,9 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
def data(self, index, role):
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == ITEM_ID_ROLE:
|
||||
return item["id"]
|
||||
|
||||
if role == self.IdRole:
|
||||
return item.get("_id")
|
||||
|
||||
|
|
@ -1134,12 +1153,14 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
if len(self.version_ids) > 1:
|
||||
group = repre_groups.get(doc["name"])
|
||||
if not group:
|
||||
|
||||
group_item = Item()
|
||||
item_id = str(uuid4())
|
||||
group_item.update({
|
||||
"id": item_id,
|
||||
"_id": doc["_id"],
|
||||
"name": doc["name"],
|
||||
"isMerged": True,
|
||||
"childRow": 0,
|
||||
"active_site_name": self.active_site,
|
||||
"remote_site_name": self.remote_site,
|
||||
"icon": qtawesome.icon(
|
||||
|
|
@ -1147,6 +1168,7 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
color=style.colors.default
|
||||
)
|
||||
})
|
||||
self._items_by_id[item_id] = group_item
|
||||
self.add_child(group_item, None)
|
||||
repre_groups[doc["name"]] = group_item
|
||||
repre_groups_items[doc["name"]] = 0
|
||||
|
|
@ -1159,7 +1181,9 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
active_site_icon = self._icons.get(self.active_provider)
|
||||
remote_site_icon = self._icons.get(self.remote_provider)
|
||||
|
||||
item_id = str(uuid4())
|
||||
data = {
|
||||
"id": item_id,
|
||||
"_id": doc["_id"],
|
||||
"name": doc["name"],
|
||||
"subset": doc["context"]["subset"],
|
||||
|
|
@ -1178,6 +1202,7 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
|
||||
item = Item()
|
||||
item.update(data)
|
||||
self._items_by_id[item_id] = item
|
||||
|
||||
current_progress = {
|
||||
'active_site_progress': progress[self.active_site],
|
||||
|
|
@ -1201,6 +1226,9 @@ class RepresentationModel(TreeModel, BaseRepresentationModel):
|
|||
self.endResetModel()
|
||||
self.refreshed.emit(False)
|
||||
|
||||
def get_item_by_id(self, item_id):
|
||||
return self._items_by_id.get(item_id)
|
||||
|
||||
def refresh(self):
|
||||
docs = []
|
||||
session_project = self.dbcon.Session['AVALON_PROJECT']
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ from .model import (
|
|||
SubsetFilterProxyModel,
|
||||
FamiliesFilterProxyModel,
|
||||
RepresentationModel,
|
||||
RepresentationSortProxyModel
|
||||
RepresentationSortProxyModel,
|
||||
ITEM_ID_ROLE
|
||||
)
|
||||
from . import lib
|
||||
|
||||
|
|
@ -351,6 +352,59 @@ class SubsetWidget(QtWidgets.QWidget):
|
|||
|
||||
lib.change_visibility(self.model, self.view, "repre_info", enabled)
|
||||
|
||||
def get_selected_items(self):
|
||||
selection_model = self.view.selectionModel()
|
||||
indexes = selection_model.selectedIndexes()
|
||||
|
||||
item_ids = set()
|
||||
for index in indexes:
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id is not None:
|
||||
item_ids.add(item_id)
|
||||
|
||||
output = []
|
||||
for item_id in item_ids:
|
||||
item = self.model.get_item_by_id(item_id)
|
||||
if item is not None:
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
def get_selected_merge_items(self):
|
||||
output = []
|
||||
items = collections.deque(self.get_selected_items())
|
||||
|
||||
item_ids = set()
|
||||
while items:
|
||||
item = items.popleft()
|
||||
if item.get("isGroup"):
|
||||
for child in item.children():
|
||||
items.appendleft(child)
|
||||
|
||||
elif item.get("isMerged"):
|
||||
item_id = item["id"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.add(item_id)
|
||||
output.append(item)
|
||||
|
||||
return output
|
||||
|
||||
def get_selected_subsets(self):
|
||||
output = []
|
||||
items = collections.deque(self.get_selected_items())
|
||||
|
||||
item_ids = set()
|
||||
while items:
|
||||
item = items.popleft()
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
for child in item.children():
|
||||
items.appendleft(child)
|
||||
else:
|
||||
item_id = item["id"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.add(item_id)
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
def on_context_menu(self, point):
|
||||
"""Shows menu with loader actions on Right-click.
|
||||
|
||||
|
|
@ -367,10 +421,7 @@ class SubsetWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
# Get selected subsets without groups
|
||||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows(column=0)
|
||||
|
||||
items = lib.get_selected_items(rows, self.model.ItemRole)
|
||||
items = self.get_selected_subsets()
|
||||
|
||||
# Get all representation->loader combinations available for the
|
||||
# index under the cursor, so we can list the user the options.
|
||||
|
|
@ -539,35 +590,6 @@ class SubsetWidget(QtWidgets.QWidget):
|
|||
box = LoadErrorMessageBox(error_info, self)
|
||||
box.show()
|
||||
|
||||
def selected_subsets(self, _groups=False, _merged=False, _other=True):
|
||||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows(column=0)
|
||||
|
||||
subsets = list()
|
||||
if not any([_groups, _merged, _other]):
|
||||
self.echo((
|
||||
"This is a BUG: Selected_subsets args must contain"
|
||||
" at least one value set to True"
|
||||
))
|
||||
return subsets
|
||||
|
||||
for row in rows:
|
||||
item = row.data(self.model.ItemRole)
|
||||
if item.get("isGroup"):
|
||||
if not _groups:
|
||||
continue
|
||||
|
||||
elif item.get("isMerged"):
|
||||
if not _merged:
|
||||
continue
|
||||
else:
|
||||
if not _other:
|
||||
continue
|
||||
|
||||
subsets.append(item)
|
||||
|
||||
return subsets
|
||||
|
||||
def group_subsets(self, name, asset_ids, items):
|
||||
field = "data.subsetGroup"
|
||||
|
||||
|
|
@ -1261,6 +1283,40 @@ class RepresentationWidget(QtWidgets.QWidget):
|
|||
}
|
||||
return repre_context_by_id
|
||||
|
||||
def get_selected_items(self):
|
||||
selection_model = self.tree_view.selectionModel()
|
||||
indexes = selection_model.selectedIndexes()
|
||||
|
||||
item_ids = set()
|
||||
for index in indexes:
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id is not None:
|
||||
item_ids.add(item_id)
|
||||
|
||||
output = []
|
||||
for item_id in item_ids:
|
||||
item = self.model.get_item_by_id(item_id)
|
||||
if item is not None:
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
def get_selected_repre_items(self):
|
||||
output = []
|
||||
items = collections.deque(self.get_selected_items())
|
||||
|
||||
item_ids = set()
|
||||
while items:
|
||||
item = items.popleft()
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
for child in item.children():
|
||||
items.appendleft(child)
|
||||
else:
|
||||
item_id = item["id"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.add(item_id)
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
def on_context_menu(self, point):
|
||||
"""Shows menu with loader actions on Right-click.
|
||||
|
||||
|
|
@ -1279,10 +1335,8 @@ class RepresentationWidget(QtWidgets.QWidget):
|
|||
selection = self.tree_view.selectionModel()
|
||||
rows = selection.selectedRows(column=0)
|
||||
|
||||
items = lib.get_selected_items(rows, self.model.ItemRole)
|
||||
|
||||
items = self.get_selected_repre_items()
|
||||
selected_side = self._get_selected_side(point_index, rows)
|
||||
|
||||
# Get all representation->loader combinations available for the
|
||||
# index under the cursor, so we can list the user the options.
|
||||
available_loaders = api.discover(api.Loader)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ def get_all_asset_nodes():
|
|||
|
||||
# Gather all information
|
||||
container_name = container["objectName"]
|
||||
nodes += cmds.sets(container_name, query=True, nodesOnly=True) or []
|
||||
nodes += lib.get_container_members(container_name)
|
||||
|
||||
nodes = list(set(nodes))
|
||||
return nodes
|
||||
|
|
@ -195,7 +195,7 @@ def remove_unused_looks():
|
|||
unused = []
|
||||
for container in host.ls():
|
||||
if container['loader'] == "LookLoader":
|
||||
members = cmds.sets(container['objectName'], query=True)
|
||||
members = lib.get_container_members(container['objectName'])
|
||||
look_sets = cmds.ls(members, type="objectSet")
|
||||
for look_set in look_sets:
|
||||
# If the set is used than we consider this look *in use*
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ from collections import defaultdict
|
|||
|
||||
from Qt import QtCore
|
||||
|
||||
from avalon.tools import models
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.style import colors
|
||||
from openpype.tools.utils import models
|
||||
|
||||
|
||||
class AssetModel(models.TreeModel):
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ import six
|
|||
import alembic.Abc
|
||||
from maya import cmds
|
||||
|
||||
import avalon.io as io
|
||||
import avalon.maya
|
||||
import avalon.api as api
|
||||
from avalon import io, api
|
||||
|
||||
import openpype.hosts.maya.api.lib as lib
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -203,10 +201,12 @@ def load_look(version_id):
|
|||
raise RuntimeError("Could not find LookLoader, this is a bug")
|
||||
|
||||
# Reference the look file
|
||||
with avalon.maya.maintained_selection():
|
||||
with lib.maintained_selection():
|
||||
container_node = api.load(loader, look_representation)
|
||||
|
||||
return cmds.sets(container_node, query=True)
|
||||
# Get container members
|
||||
shader_nodes = lib.get_container_members(container_node)
|
||||
return shader_nodes
|
||||
|
||||
|
||||
def get_latest_version(asset_id, subset):
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ from . import views
|
|||
|
||||
from maya import cmds
|
||||
|
||||
MODELINDEX = QtCore.QModelIndex()
|
||||
|
||||
|
||||
class AssetOutliner(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ from Qt import QtWidgets, QtCore
|
|||
|
||||
from .widgets import (
|
||||
NameTextEdit,
|
||||
FilterComboBox
|
||||
FilterComboBox,
|
||||
SpinBoxScrollFixed,
|
||||
DoubleSpinBoxScrollFixed
|
||||
)
|
||||
from .multiselection_combobox import MultiSelectionComboBox
|
||||
|
||||
|
|
@ -89,9 +91,9 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
def createEditor(self, parent, option, index):
|
||||
if self.decimals > 0:
|
||||
editor = QtWidgets.QDoubleSpinBox(parent)
|
||||
editor = DoubleSpinBoxScrollFixed(parent)
|
||||
else:
|
||||
editor = QtWidgets.QSpinBox(parent)
|
||||
editor = SpinBoxScrollFixed(parent)
|
||||
|
||||
editor.setObjectName("NumberEditor")
|
||||
# Set min/max
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
from Qt import QtCore, QtGui
|
||||
|
||||
from openpype.style import get_objected_colors
|
||||
from avalon.vendor import qtawesome
|
||||
from openpype.tools.utils import paint_image_with_color
|
||||
|
||||
|
||||
class ResourceCache:
|
||||
|
|
@ -91,17 +91,6 @@ class ResourceCache:
|
|||
icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)
|
||||
return icon
|
||||
|
||||
@classmethod
|
||||
def get_warning_pixmap(cls):
|
||||
src_image = get_warning_image()
|
||||
colors = get_objected_colors()
|
||||
color_value = colors["delete-btn-bg"]
|
||||
|
||||
return paint_image_with_color(
|
||||
src_image,
|
||||
color_value.get_qcolor()
|
||||
)
|
||||
|
||||
|
||||
def get_remove_image():
|
||||
image_path = os.path.join(
|
||||
|
|
@ -110,36 +99,3 @@ def get_remove_image():
|
|||
"bin.png"
|
||||
)
|
||||
return QtGui.QImage(image_path)
|
||||
|
||||
|
||||
def get_warning_image():
|
||||
image_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"images",
|
||||
"warning.png"
|
||||
)
|
||||
return QtGui.QImage(image_path)
|
||||
|
||||
|
||||
def paint_image_with_color(image, color):
|
||||
"""TODO: This function should be imported from utils.
|
||||
|
||||
At the moment of creation is not available yet.
|
||||
"""
|
||||
width = image.width()
|
||||
height = image.height()
|
||||
|
||||
alpha_mask = image.createAlphaMask()
|
||||
alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask))
|
||||
|
||||
pixmap = QtGui.QPixmap(width, height)
|
||||
pixmap.fill(QtCore.Qt.transparent)
|
||||
|
||||
painter = QtGui.QPainter(pixmap)
|
||||
painter.setClipRegion(alpha_region)
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(color)
|
||||
painter.drawRect(QtCore.QRect(0, 0, width, height))
|
||||
painter.end()
|
||||
|
||||
return pixmap
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ from .constants import (
|
|||
NAME_ALLOWED_SYMBOLS,
|
||||
NAME_REGEX
|
||||
)
|
||||
from .style import ResourceCache
|
||||
from openpype.lib import (
|
||||
create_project,
|
||||
PROJECT_NAME_ALLOWED_SYMBOLS,
|
||||
PROJECT_NAME_REGEX
|
||||
)
|
||||
from openpype.style import load_stylesheet
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
get_warning_pixmap
|
||||
)
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -338,7 +340,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
|
|||
|
||||
top_widget = QtWidgets.QWidget(self)
|
||||
|
||||
warning_pixmap = ResourceCache.get_warning_pixmap()
|
||||
warning_pixmap = get_warning_pixmap()
|
||||
warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
|
||||
|
||||
message_label = QtWidgets.QLabel(top_widget)
|
||||
|
|
@ -429,3 +431,29 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
|
|||
def _on_confirm_text_change(self):
|
||||
enabled = self._confirm_input.text() == self._project_name
|
||||
self._confirm_btn.setEnabled(enabled)
|
||||
|
||||
|
||||
class SpinBoxScrollFixed(QtWidgets.QSpinBox):
|
||||
"""QSpinBox which only allow edits change with scroll wheel when active"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SpinBoxScrollFixed, self).__init__(*args, **kwargs)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if not self.hasFocus():
|
||||
event.ignore()
|
||||
else:
|
||||
super(SpinBoxScrollFixed, self).wheelEvent(event)
|
||||
|
||||
|
||||
class DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox):
|
||||
"""QDoubleSpinBox which only allow edits with scroll wheel when active"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DoubleSpinBoxScrollFixed, self).__init__(*args, **kwargs)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if not self.hasFocus():
|
||||
event.ignore()
|
||||
else:
|
||||
super(DoubleSpinBoxScrollFixed, self).wheelEvent(event)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,9 @@ class ProjectManagerWindow(QtWidgets.QWidget):
|
|||
helper_btns_widget
|
||||
)
|
||||
add_asset_btn.setObjectName("IconBtn")
|
||||
add_asset_btn.setEnabled(False)
|
||||
add_task_btn.setObjectName("IconBtn")
|
||||
add_task_btn.setEnabled(False)
|
||||
|
||||
helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget)
|
||||
helper_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -138,6 +140,7 @@ class ProjectManagerWindow(QtWidgets.QWidget):
|
|||
|
||||
message_label = QtWidgets.QLabel(buttons_widget)
|
||||
save_btn = QtWidgets.QPushButton("Save", buttons_widget)
|
||||
save_btn.setEnabled(False)
|
||||
|
||||
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
|
||||
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -173,6 +176,7 @@ class ProjectManagerWindow(QtWidgets.QWidget):
|
|||
self._create_project_btn = create_project_btn
|
||||
self._create_folders_btn = create_folders_btn
|
||||
self._remove_projects_btn = remove_projects_btn
|
||||
self._save_btn = save_btn
|
||||
|
||||
self._add_asset_btn = add_asset_btn
|
||||
self._add_task_btn = add_task_btn
|
||||
|
|
@ -183,6 +187,9 @@ class ProjectManagerWindow(QtWidgets.QWidget):
|
|||
def _set_project(self, project_name=None):
|
||||
self._create_folders_btn.setEnabled(project_name is not None)
|
||||
self._remove_projects_btn.setEnabled(project_name is not None)
|
||||
self._add_asset_btn.setEnabled(project_name is not None)
|
||||
self._add_task_btn.setEnabled(project_name is not None)
|
||||
self._save_btn.setEnabled(project_name is not None)
|
||||
self._project_proxy_model.set_filter_default(project_name is not None)
|
||||
self.hierarchy_view.set_project(project_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -605,7 +605,9 @@ class PublisherController:
|
|||
found_idx = idx
|
||||
break
|
||||
|
||||
value = instance.creator_attributes[attr_def.key]
|
||||
value = None
|
||||
if attr_def.is_value_def:
|
||||
value = instance.creator_attributes[attr_def.key]
|
||||
if found_idx is None:
|
||||
idx = len(output)
|
||||
output.append((attr_def, [instance], [value]))
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ from .border_label_widget import (
|
|||
from .widgets import (
|
||||
SubsetAttributesWidget,
|
||||
|
||||
PixmapLabel,
|
||||
|
||||
StopBtn,
|
||||
ResetBtn,
|
||||
ValidateBtn,
|
||||
|
|
@ -44,8 +42,6 @@ __all__ = (
|
|||
"SubsetAttributesWidget",
|
||||
"BorderedLabelWidget",
|
||||
|
||||
"PixmapLabel",
|
||||
|
||||
"StopBtn",
|
||||
"ResetBtn",
|
||||
"ValidateBtn",
|
||||
|
|
|
|||
273
openpype/tools/publisher/widgets/assets_widget.py
Normal file
273
openpype/tools/publisher/widgets/assets_widget.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import collections
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
RecursiveSortFilterProxyModel
|
||||
)
|
||||
from openpype.tools.utils.assets_widget import (
|
||||
SingleSelectAssetsWidget,
|
||||
ASSET_ID_ROLE,
|
||||
ASSET_NAME_ROLE
|
||||
)
|
||||
|
||||
|
||||
class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
|
||||
current_context_required = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
super(CreateDialogAssetsWidget, self).__init__(None, parent)
|
||||
|
||||
self.set_refresh_btn_visibility(False)
|
||||
self.set_current_asset_btn_visibility(False)
|
||||
|
||||
self._current_asset_name = None
|
||||
self._last_selection = None
|
||||
self._enabled = None
|
||||
|
||||
def _on_current_asset_click(self):
|
||||
self.current_context_required.emit()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if self._enabled == enabled:
|
||||
return
|
||||
self._enabled = enabled
|
||||
if not enabled:
|
||||
self._last_selection = self.get_selected_asset_id()
|
||||
self._clear_selection()
|
||||
elif self._last_selection is not None:
|
||||
self.select_asset(self._last_selection)
|
||||
|
||||
def _select_indexes(self, *args, **kwargs):
|
||||
super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs)
|
||||
if self._enabled:
|
||||
return
|
||||
self._last_selection = self.get_selected_asset_id()
|
||||
self._clear_selection()
|
||||
|
||||
def set_current_asset_name(self, asset_name):
|
||||
self._current_asset_name = asset_name
|
||||
# Hide set current asset if there is no one
|
||||
self.set_current_asset_btn_visibility(asset_name is not None)
|
||||
|
||||
def _get_current_session_asset(self):
|
||||
return self._current_asset_name
|
||||
|
||||
def _create_source_model(self):
|
||||
return AssetsHierarchyModel(self._controller)
|
||||
|
||||
def _refresh_model(self):
|
||||
self._model.reset()
|
||||
self._on_model_refresh(self._model.rowCount() > 0)
|
||||
|
||||
|
||||
class AssetsHierarchyModel(QtGui.QStandardItemModel):
|
||||
"""Assets hiearrchy model.
|
||||
|
||||
For selecting asset for which should beinstance created.
|
||||
|
||||
Uses controller to load asset hierarchy. All asset documents are stored by
|
||||
their parents.
|
||||
"""
|
||||
def __init__(self, controller):
|
||||
super(AssetsHierarchyModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._items_by_name = {}
|
||||
self._items_by_asset_id = {}
|
||||
|
||||
def reset(self):
|
||||
self.clear()
|
||||
|
||||
self._items_by_name = {}
|
||||
self._items_by_asset_id = {}
|
||||
assets_by_parent_id = self._controller.get_asset_hierarchy()
|
||||
|
||||
items_by_name = {}
|
||||
items_by_asset_id = {}
|
||||
_queue = collections.deque()
|
||||
_queue.append((self.invisibleRootItem(), None))
|
||||
while _queue:
|
||||
parent_item, parent_id = _queue.popleft()
|
||||
children = assets_by_parent_id.get(parent_id)
|
||||
if not children:
|
||||
continue
|
||||
|
||||
children_by_name = {
|
||||
child["name"]: child
|
||||
for child in children
|
||||
}
|
||||
items = []
|
||||
for name in sorted(children_by_name.keys()):
|
||||
child = children_by_name[name]
|
||||
child_id = child["_id"]
|
||||
item = QtGui.QStandardItem(name)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(child_id, ASSET_ID_ROLE)
|
||||
item.setData(name, ASSET_NAME_ROLE)
|
||||
|
||||
items_by_name[name] = item
|
||||
items_by_asset_id[child_id] = item
|
||||
items.append(item)
|
||||
_queue.append((item, child_id))
|
||||
|
||||
parent_item.appendRows(items)
|
||||
|
||||
self._items_by_name = items_by_name
|
||||
self._items_by_asset_id = items_by_asset_id
|
||||
|
||||
def get_index_by_asset_id(self, asset_id):
|
||||
item = self._items_by_asset_id.get(asset_id)
|
||||
if item is not None:
|
||||
return item.index()
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def get_index_by_asset_name(self, asset_name):
|
||||
item = self._items_by_name.get(asset_name)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return item.index()
|
||||
|
||||
def name_is_valid(self, item_name):
|
||||
return item_name in self._items_by_name
|
||||
|
||||
|
||||
class AssetsDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select asset for a context of instance."""
|
||||
def __init__(self, controller, parent):
|
||||
super(AssetsDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Select asset")
|
||||
|
||||
model = AssetsHierarchyModel(controller)
|
||||
proxy_model = RecursiveSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
filter_input.setPlaceholderText("Filter assets..")
|
||||
|
||||
asset_view = QtWidgets.QTreeView(self)
|
||||
asset_view.setModel(proxy_model)
|
||||
asset_view.setHeaderHidden(True)
|
||||
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
asset_view.setAlternatingRowColors(True)
|
||||
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
|
||||
asset_view.setAllColumnsShowFocus(True)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(filter_input, 0)
|
||||
layout.addWidget(asset_view, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
filter_input.textChanged.connect(self._on_filter_change)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._filter_input = filter_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._asset_view = asset_view
|
||||
|
||||
self._selected_asset = None
|
||||
# Soft refresh is enabled
|
||||
# - reset will happen at all cost if soft reset is enabled
|
||||
# - adds ability to call reset on multiple places without repeating
|
||||
self._soft_reset_enabled = True
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Refresh asset model on show."""
|
||||
super(AssetsDialog, self).showEvent(event)
|
||||
# Refresh on show
|
||||
self.reset(False)
|
||||
|
||||
def reset(self, force=True):
|
||||
"""Reset asset model."""
|
||||
if not force and not self._soft_reset_enabled:
|
||||
return
|
||||
|
||||
if self._soft_reset_enabled:
|
||||
self._soft_reset_enabled = False
|
||||
|
||||
self._model.reset()
|
||||
|
||||
def name_is_valid(self, name):
|
||||
"""Is asset name valid.
|
||||
|
||||
Args:
|
||||
name(str): Asset name that should be checked.
|
||||
"""
|
||||
# Make sure we're reset
|
||||
self.reset(False)
|
||||
# Valid the name by model
|
||||
return self._model.name_is_valid(name)
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of assets."""
|
||||
self._proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self.done(0)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
index = self._asset_view.currentIndex()
|
||||
asset_name = None
|
||||
if index.isValid():
|
||||
asset_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._selected_asset = asset_name
|
||||
self.done(1)
|
||||
|
||||
def set_selected_assets(self, asset_names):
|
||||
"""Change preselected asset before showing the dialog.
|
||||
|
||||
This also resets model and clean filter.
|
||||
"""
|
||||
self.reset(False)
|
||||
self._asset_view.collapseAll()
|
||||
self._filter_input.setText("")
|
||||
|
||||
indexes = []
|
||||
for asset_name in asset_names:
|
||||
index = self._model.get_index_by_asset_name(asset_name)
|
||||
if index.isValid():
|
||||
indexes.append(index)
|
||||
|
||||
if not indexes:
|
||||
return
|
||||
|
||||
index_deque = collections.deque()
|
||||
for index in indexes:
|
||||
index_deque.append(index)
|
||||
|
||||
all_indexes = []
|
||||
while index_deque:
|
||||
index = index_deque.popleft()
|
||||
all_indexes.append(index)
|
||||
|
||||
parent_index = index.parent()
|
||||
if parent_index.isValid():
|
||||
index_deque.append(parent_index)
|
||||
|
||||
for index in all_indexes:
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._asset_view.expand(proxy_index)
|
||||
|
||||
def get_selected_asset(self):
|
||||
"""Get selected asset name."""
|
||||
return self._selected_asset
|
||||
|
|
@ -27,12 +27,12 @@ from Qt import QtWidgets, QtCore
|
|||
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame
|
||||
from .widgets import (
|
||||
AbstractInstanceView,
|
||||
ContextWarningLabel,
|
||||
ClickableFrame,
|
||||
IconValuePixmapLabel,
|
||||
TransparentPixmapLabel
|
||||
PublishPixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
CONTEXT_ID,
|
||||
|
|
@ -140,7 +140,7 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
widget_idx += 1
|
||||
|
||||
|
||||
class CardWidget(ClickableFrame):
|
||||
class CardWidget(BaseClickableFrame):
|
||||
"""Clickable card used as bigger button."""
|
||||
selected = QtCore.Signal(str, str)
|
||||
# Group identifier of card
|
||||
|
|
@ -184,7 +184,7 @@ class ContextCardWidget(CardWidget):
|
|||
self._id = CONTEXT_ID
|
||||
self._group_identifier = ""
|
||||
|
||||
icon_widget = TransparentPixmapLabel(self)
|
||||
icon_widget = PublishPixmapLabel(None, self)
|
||||
icon_widget.setObjectName("FamilyIconLabel")
|
||||
|
||||
label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ from openpype.pipeline.create import (
|
|||
)
|
||||
|
||||
from .widgets import IconValuePixmapLabel
|
||||
from .assets_widget import CreateDialogAssetsWidget
|
||||
from .tasks_widget import CreateDialogTasksWidget
|
||||
from .precreate_widget import PreCreateWidget
|
||||
from ..constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
CREATOR_IDENTIFIER_ROLE,
|
||||
|
|
@ -202,7 +205,23 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
self._name_pattern = name_pattern
|
||||
self._compiled_name_pattern = re.compile(name_pattern)
|
||||
|
||||
context_widget = QtWidgets.QWidget(self)
|
||||
|
||||
assets_widget = CreateDialogAssetsWidget(controller, context_widget)
|
||||
tasks_widget = CreateDialogTasksWidget(controller, context_widget)
|
||||
|
||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||
context_layout.setSpacing(0)
|
||||
context_layout.addWidget(assets_widget, 2)
|
||||
context_layout.addWidget(tasks_widget, 1)
|
||||
|
||||
# Precreate attributes widgets
|
||||
pre_create_widget = PreCreateWidget(self)
|
||||
|
||||
# TODO add HELP button
|
||||
creator_description_widget = CreatorDescriptionWidget(self)
|
||||
creator_description_widget.setVisible(False)
|
||||
|
||||
creators_view = QtWidgets.QListView(self)
|
||||
creators_model = QtGui.QStandardItemModel()
|
||||
|
|
@ -235,27 +254,46 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
form_layout.addRow("Name:", variant_layout)
|
||||
form_layout.addRow("Subset:", subset_name_input)
|
||||
|
||||
left_layout = QtWidgets.QVBoxLayout()
|
||||
left_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
|
||||
left_layout.addWidget(creators_view, 1)
|
||||
left_layout.addLayout(form_layout, 0)
|
||||
left_layout.addWidget(create_btn, 0)
|
||||
mid_widget = QtWidgets.QWidget(self)
|
||||
mid_layout = QtWidgets.QVBoxLayout(mid_widget)
|
||||
mid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
|
||||
mid_layout.addWidget(creators_view, 1)
|
||||
mid_layout.addLayout(form_layout, 0)
|
||||
mid_layout.addWidget(create_btn, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addLayout(left_layout, 0)
|
||||
layout.addSpacing(5)
|
||||
layout.addWidget(creator_description_widget, 1)
|
||||
layout.setSpacing(10)
|
||||
layout.addWidget(context_widget, 1)
|
||||
layout.addWidget(mid_widget, 1)
|
||||
layout.addWidget(pre_create_widget, 1)
|
||||
|
||||
prereq_timer = QtCore.QTimer()
|
||||
prereq_timer.setInterval(50)
|
||||
prereq_timer.setSingleShot(True)
|
||||
|
||||
prereq_timer.timeout.connect(self._on_prereq_timer)
|
||||
|
||||
create_btn.clicked.connect(self._on_create)
|
||||
variant_input.returnPressed.connect(self._on_create)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
creators_view.selectionModel().currentChanged.connect(
|
||||
self._on_item_change
|
||||
self._on_creator_item_change
|
||||
)
|
||||
variant_hints_menu.triggered.connect(self._on_variant_action)
|
||||
assets_widget.selection_changed.connect(self._on_asset_change)
|
||||
assets_widget.current_context_required.connect(
|
||||
self._on_current_session_context_request
|
||||
)
|
||||
tasks_widget.task_changed.connect(self._on_task_change)
|
||||
|
||||
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
|
||||
|
||||
self._pre_create_widget = pre_create_widget
|
||||
|
||||
self._context_widget = context_widget
|
||||
self._assets_widget = assets_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
self.creator_description_widget = creator_description_widget
|
||||
|
||||
self.subset_name_input = subset_name_input
|
||||
|
|
@ -269,12 +307,54 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
self.creators_view = creators_view
|
||||
self.create_btn = create_btn
|
||||
|
||||
self._prereq_timer = prereq_timer
|
||||
|
||||
def _context_change_is_enabled(self):
|
||||
return self._context_widget.isEnabled()
|
||||
|
||||
def _get_asset_name(self):
|
||||
asset_name = None
|
||||
if self._context_change_is_enabled():
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
|
||||
if asset_name is None:
|
||||
asset_name = self._asset_name
|
||||
return asset_name
|
||||
|
||||
def _get_task_name(self):
|
||||
task_name = None
|
||||
if self._context_change_is_enabled():
|
||||
# Don't use selection of task if asset is not set
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
if asset_name:
|
||||
task_name = self._tasks_widget.get_selected_task_name()
|
||||
|
||||
if not task_name:
|
||||
task_name = self._task_name
|
||||
return task_name
|
||||
|
||||
@property
|
||||
def dbcon(self):
|
||||
return self.controller.dbcon
|
||||
|
||||
def _set_context_enabled(self, enabled):
|
||||
self._assets_widget.set_enabled(enabled)
|
||||
self._tasks_widget.set_enabled(enabled)
|
||||
self._context_widget.setEnabled(enabled)
|
||||
|
||||
def refresh(self):
|
||||
self._prereq_available = True
|
||||
# Get context before refresh to keep selection of asset and
|
||||
# task widgets
|
||||
asset_name = self._get_asset_name()
|
||||
task_name = self._get_task_name()
|
||||
|
||||
self._prereq_available = False
|
||||
|
||||
# Disable context widget so refresh of asset will use context asset
|
||||
# name
|
||||
self._set_context_enabled(False)
|
||||
|
||||
self._assets_widget.refresh()
|
||||
|
||||
# Refresh data before update of creators
|
||||
self._refresh_asset()
|
||||
|
|
@ -282,21 +362,36 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
# data
|
||||
self._refresh_creators()
|
||||
|
||||
self._assets_widget.set_current_asset_name(self._asset_name)
|
||||
self._assets_widget.select_asset_by_name(asset_name)
|
||||
self._tasks_widget.set_asset_name(asset_name)
|
||||
self._tasks_widget.select_task_name(task_name)
|
||||
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _invalidate_prereq(self):
|
||||
self._prereq_timer.start()
|
||||
|
||||
def _on_prereq_timer(self):
|
||||
prereq_available = True
|
||||
if self.creators_model.rowCount() < 1:
|
||||
prereq_available = False
|
||||
|
||||
if self._asset_doc is None:
|
||||
# QUESTION how to handle invalid asset?
|
||||
self.subset_name_input.setText("< Asset is not set >")
|
||||
self._prereq_available = False
|
||||
prereq_available = False
|
||||
|
||||
if self.creators_model.rowCount() < 1:
|
||||
self._prereq_available = False
|
||||
if prereq_available != self._prereq_available:
|
||||
self._prereq_available = prereq_available
|
||||
|
||||
self.create_btn.setEnabled(self._prereq_available)
|
||||
self.creators_view.setEnabled(self._prereq_available)
|
||||
self.variant_input.setEnabled(self._prereq_available)
|
||||
self.variant_hints_btn.setEnabled(self._prereq_available)
|
||||
self.create_btn.setEnabled(prereq_available)
|
||||
self.creators_view.setEnabled(prereq_available)
|
||||
self.variant_input.setEnabled(prereq_available)
|
||||
self.variant_hints_btn.setEnabled(prereq_available)
|
||||
self._on_variant_change()
|
||||
|
||||
def _refresh_asset(self):
|
||||
asset_name = self._asset_name
|
||||
asset_name = self._get_asset_name()
|
||||
|
||||
# Skip if asset did not change
|
||||
if self._asset_doc and self._asset_doc["name"] == asset_name:
|
||||
|
|
@ -324,6 +419,9 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
)
|
||||
self._subset_names = set(subset_docs.distinct("name"))
|
||||
|
||||
if not asset_doc:
|
||||
self.subset_name_input.setText("< Asset is not set >")
|
||||
|
||||
def _refresh_creators(self):
|
||||
# Refresh creators and add their families to list
|
||||
existing_items = {}
|
||||
|
|
@ -366,25 +464,60 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
if not indexes:
|
||||
index = self.creators_model.index(0, 0)
|
||||
self.creators_view.setCurrentIndex(index)
|
||||
else:
|
||||
index = indexes[0]
|
||||
|
||||
identifier = index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
|
||||
self._set_creator(identifier)
|
||||
|
||||
def _on_plugins_refresh(self):
|
||||
# Trigger refresh only if is visible
|
||||
if self.isVisible():
|
||||
self.refresh()
|
||||
|
||||
def _on_item_change(self, new_index, _old_index):
|
||||
def _on_asset_change(self):
|
||||
self._refresh_asset()
|
||||
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
self._tasks_widget.set_asset_name(asset_name)
|
||||
if self._context_change_is_enabled():
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _on_task_change(self):
|
||||
if self._context_change_is_enabled():
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _on_current_session_context_request(self):
|
||||
self._assets_widget.set_current_session_asset()
|
||||
if self._task_name:
|
||||
self._tasks_widget.select_task_name(self._task_name)
|
||||
|
||||
def _on_creator_item_change(self, new_index, _old_index):
|
||||
identifier = None
|
||||
if new_index.isValid():
|
||||
identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
self._set_creator(identifier)
|
||||
|
||||
def _set_creator(self, identifier):
|
||||
creator = self.controller.manual_creators.get(identifier)
|
||||
|
||||
self.creator_description_widget.set_plugin(creator)
|
||||
self._pre_create_widget.set_plugin(creator)
|
||||
|
||||
self._selected_creator = creator
|
||||
|
||||
if not creator:
|
||||
self._set_context_enabled(False)
|
||||
return
|
||||
|
||||
if (
|
||||
creator.create_allow_context_change
|
||||
!= self._context_change_is_enabled()
|
||||
):
|
||||
self._set_context_enabled(creator.create_allow_context_change)
|
||||
self._refresh_asset()
|
||||
|
||||
default_variants = creator.get_default_variants()
|
||||
if not default_variants:
|
||||
default_variants = ["Main"]
|
||||
|
|
@ -410,12 +543,19 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
if self.variant_input.text() != value:
|
||||
self.variant_input.setText(value)
|
||||
|
||||
def _on_variant_change(self, variant_value):
|
||||
if not self._prereq_available or not self._selected_creator:
|
||||
def _on_variant_change(self, variant_value=None):
|
||||
if not self._prereq_available:
|
||||
return
|
||||
|
||||
# This should probably never happen?
|
||||
if not self._selected_creator:
|
||||
if self.subset_name_input.text():
|
||||
self.subset_name_input.setText("")
|
||||
return
|
||||
|
||||
if variant_value is None:
|
||||
variant_value = self.variant_input.text()
|
||||
|
||||
match = self._compiled_name_pattern.match(variant_value)
|
||||
valid = bool(match)
|
||||
self.create_btn.setEnabled(valid)
|
||||
|
|
@ -425,7 +565,7 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
return
|
||||
|
||||
project_name = self.controller.project_name
|
||||
task_name = self._task_name
|
||||
task_name = self._get_task_name()
|
||||
|
||||
asset_doc = copy.deepcopy(self._asset_doc)
|
||||
# Calculate subset name with Creator plugin
|
||||
|
|
@ -522,9 +662,9 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
family = index.data(FAMILY_ROLE)
|
||||
subset_name = self.subset_name_input.text()
|
||||
variant = self.variant_input.text()
|
||||
asset_name = self._asset_name
|
||||
task_name = self._task_name
|
||||
options = {}
|
||||
asset_name = self._get_asset_name()
|
||||
task_name = self._get_task_name()
|
||||
pre_create_data = self._pre_create_widget.current_value()
|
||||
# Where to define these data?
|
||||
# - what data show be stored?
|
||||
instance_data = {
|
||||
|
|
@ -537,7 +677,7 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
error_info = None
|
||||
try:
|
||||
self.controller.create(
|
||||
creator_identifier, subset_name, instance_data, options
|
||||
creator_identifier, subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
|
|
|
|||
133
openpype/tools/publisher/widgets/precreate_widget.py
Normal file
133
openpype/tools/publisher/widgets/precreate_widget.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
||||
|
||||
|
||||
class PreCreateWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PreCreateWidget, self).__init__(parent)
|
||||
|
||||
# Precreate attribute defininitions of Creator
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
contet_widget = QtWidgets.QWidget(scroll_area)
|
||||
scroll_area.setWidget(contet_widget)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
||||
attributes_widget = AttributesWidget(contet_widget)
|
||||
contet_layout = QtWidgets.QVBoxLayout(contet_widget)
|
||||
contet_layout.setContentsMargins(0, 0, 0, 0)
|
||||
contet_layout.addWidget(attributes_widget, 0)
|
||||
contet_layout.addStretch(1)
|
||||
|
||||
# Widget showed when there are no attribute definitions from creator
|
||||
empty_widget = QtWidgets.QWidget(self)
|
||||
empty_widget.setVisible(False)
|
||||
|
||||
# Label showed when creator is not selected
|
||||
no_creator_label = QtWidgets.QLabel(
|
||||
"Creator is not selected",
|
||||
empty_widget
|
||||
)
|
||||
no_creator_label.setWordWrap(True)
|
||||
|
||||
# Creator does not have precreate attributes
|
||||
empty_label = QtWidgets.QLabel(
|
||||
"This creator has no configurable options",
|
||||
empty_widget
|
||||
)
|
||||
empty_label.setWordWrap(True)
|
||||
empty_label.setVisible(False)
|
||||
|
||||
empty_layout = QtWidgets.QVBoxLayout(empty_widget)
|
||||
empty_layout.setContentsMargins(0, 0, 0, 0)
|
||||
empty_layout.addWidget(empty_label, 0, QtCore.Qt.AlignCenter)
|
||||
empty_layout.addWidget(no_creator_label, 0, QtCore.Qt.AlignCenter)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(scroll_area, 1)
|
||||
main_layout.addWidget(empty_widget, 1)
|
||||
|
||||
self._scroll_area = scroll_area
|
||||
self._empty_widget = empty_widget
|
||||
|
||||
self._empty_label = empty_label
|
||||
self._no_creator_label = no_creator_label
|
||||
self._attributes_widget = attributes_widget
|
||||
|
||||
def current_value(self):
|
||||
return self._attributes_widget.current_value()
|
||||
|
||||
def set_plugin(self, creator):
|
||||
attr_defs = []
|
||||
creator_selected = False
|
||||
if creator is not None:
|
||||
creator_selected = True
|
||||
attr_defs = creator.get_pre_create_attr_defs()
|
||||
|
||||
self._attributes_widget.set_attr_defs(attr_defs)
|
||||
|
||||
attr_defs_available = len(attr_defs) > 0
|
||||
self._scroll_area.setVisible(attr_defs_available)
|
||||
self._empty_widget.setVisible(not attr_defs_available)
|
||||
|
||||
self._empty_label.setVisible(creator_selected)
|
||||
self._no_creator_label.setVisible(not creator_selected)
|
||||
|
||||
|
||||
class AttributesWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(AttributesWidget, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self._layout = layout
|
||||
|
||||
self._widgets = []
|
||||
|
||||
def current_value(self):
|
||||
output = {}
|
||||
for widget in self._widgets:
|
||||
attr_def = widget.attr_def
|
||||
if attr_def.is_value_def:
|
||||
output[attr_def.key] = widget.current_value()
|
||||
return output
|
||||
|
||||
def clear_attr_defs(self):
|
||||
while self._layout.count():
|
||||
item = self._layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
|
||||
self._widgets = []
|
||||
|
||||
def set_attr_defs(self, attr_defs):
|
||||
self.clear_attr_defs()
|
||||
|
||||
row = 0
|
||||
for attr_def in attr_defs:
|
||||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
|
||||
expand_cols = 2
|
||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||
expand_cols = 1
|
||||
|
||||
col_num = 2 - expand_cols
|
||||
|
||||
if attr_def.label:
|
||||
label_widget = QtWidgets.QLabel(attr_def.label, self)
|
||||
self._layout.addWidget(
|
||||
label_widget, row, 0, 1, expand_cols
|
||||
)
|
||||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
|
||||
self._layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
)
|
||||
self._widgets.append(widget)
|
||||
|
||||
row += 1
|
||||
|
|
@ -1,62 +1,6 @@
|
|||
import re
|
||||
import collections
|
||||
|
||||
from Qt import QtCore, QtGui
|
||||
|
||||
|
||||
class AssetsHierarchyModel(QtGui.QStandardItemModel):
|
||||
"""Assets hiearrchy model.
|
||||
|
||||
For selecting asset for which should beinstance created.
|
||||
|
||||
Uses controller to load asset hierarchy. All asset documents are stored by
|
||||
their parents.
|
||||
"""
|
||||
def __init__(self, controller):
|
||||
super(AssetsHierarchyModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._items_by_name = {}
|
||||
|
||||
def reset(self):
|
||||
self.clear()
|
||||
|
||||
self._items_by_name = {}
|
||||
assets_by_parent_id = self._controller.get_asset_hierarchy()
|
||||
|
||||
items_by_name = {}
|
||||
_queue = collections.deque()
|
||||
_queue.append((self.invisibleRootItem(), None))
|
||||
while _queue:
|
||||
parent_item, parent_id = _queue.popleft()
|
||||
children = assets_by_parent_id.get(parent_id)
|
||||
if not children:
|
||||
continue
|
||||
|
||||
children_by_name = {
|
||||
child["name"]: child
|
||||
for child in children
|
||||
}
|
||||
items = []
|
||||
for name in sorted(children_by_name.keys()):
|
||||
child = children_by_name[name]
|
||||
item = QtGui.QStandardItem(name)
|
||||
items_by_name[name] = item
|
||||
items.append(item)
|
||||
_queue.append((item, child["_id"]))
|
||||
|
||||
parent_item.appendRows(items)
|
||||
|
||||
self._items_by_name = items_by_name
|
||||
|
||||
def name_is_valid(self, item_name):
|
||||
return item_name in self._items_by_name
|
||||
|
||||
def get_index_by_name(self, item_name):
|
||||
item = self._items_by_name.get(item_name)
|
||||
if item:
|
||||
return item.index()
|
||||
return QtCore.QModelIndex()
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget, TASK_NAME_ROLE
|
||||
|
||||
|
||||
class TasksModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -75,6 +19,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
def __init__(self, controller):
|
||||
super(TasksModel, self).__init__()
|
||||
|
||||
self._controller = controller
|
||||
self._items_by_name = {}
|
||||
self._asset_names = []
|
||||
|
|
@ -141,6 +86,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
task_names_by_asset_name = (
|
||||
self._controller.get_task_names_by_asset_names(self._asset_names)
|
||||
)
|
||||
|
||||
self._task_names_by_asset_name = task_names_by_asset_name
|
||||
|
||||
new_task_names = self.get_intersection_of_tasks(
|
||||
|
|
@ -162,40 +108,62 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
continue
|
||||
|
||||
item = QtGui.QStandardItem(task_name)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
self._items_by_name[task_name] = item
|
||||
new_items.append(item)
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
# Show nice labels in the header
|
||||
if section == 0:
|
||||
if (
|
||||
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
return "Tasks"
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Recursive proxy model.
|
||||
return super(TasksModel, self).headerData(section, orientation, role)
|
||||
|
||||
Item is not filtered if any children match the filter.
|
||||
|
||||
Use case: Filtering by string - parent won't be filtered if does not match
|
||||
the filter string but first checks if any children does.
|
||||
"""
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(
|
||||
row, self.filterKeyColumn(), parent_index
|
||||
)
|
||||
if source_index.isValid():
|
||||
pattern = regex.pattern()
|
||||
class CreateDialogTasksWidget(TasksWidget):
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
super(CreateDialogTasksWidget, self).__init__(None, parent)
|
||||
|
||||
# Check current index itself
|
||||
value = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, value, re.IGNORECASE):
|
||||
return True
|
||||
self._enabled = None
|
||||
|
||||
rows = model.rowCount(source_index)
|
||||
for idx in range(rows):
|
||||
if self.filterAcceptsRow(idx, source_index):
|
||||
return True
|
||||
return False
|
||||
def _create_source_model(self):
|
||||
return TasksModel(self._controller)
|
||||
|
||||
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
|
||||
row, parent_index
|
||||
)
|
||||
def set_asset_name(self, asset_name):
|
||||
current = self.get_selected_task_name()
|
||||
if current:
|
||||
self._last_selected_task_name = current
|
||||
|
||||
self._tasks_model.set_asset_names([asset_name])
|
||||
if self._last_selected_task_name and self._enabled:
|
||||
self.select_task_name(self._last_selected_task_name)
|
||||
|
||||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def select_task_name(self, task_name):
|
||||
super(CreateDialogTasksWidget, self).select_task_name(task_name)
|
||||
if not self._enabled:
|
||||
current = self.get_selected_task_name()
|
||||
if current:
|
||||
self._last_selected_task_name = current
|
||||
self._clear_selection()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._enabled = enabled
|
||||
if not enabled:
|
||||
last_selected_task_name = self.get_selected_task_name()
|
||||
if last_selected_task_name:
|
||||
self._last_selected_task_name = last_selected_task_name
|
||||
self._clear_selection()
|
||||
|
||||
elif self._last_selected_task_name is not None:
|
||||
self.select_task_name(self._last_selected_task_name)
|
||||
|
|
@ -6,8 +6,8 @@ except Exception:
|
|||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame
|
||||
from .widgets import (
|
||||
ClickableFrame,
|
||||
IconValuePixmapLabel
|
||||
)
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
self._error_info = error_info
|
||||
self._selected = False
|
||||
|
||||
title_frame = ClickableFrame(self)
|
||||
title_frame = BaseClickableFrame(self)
|
||||
title_frame.setObjectName("ValidationErrorTitleFrame")
|
||||
title_frame._mouse_release_callback = self._mouse_release_callback
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
|
||||
|
||||
class ActionButton(ClickableFrame):
|
||||
class ActionButton(BaseClickableFrame):
|
||||
"""Plugin's action callback button.
|
||||
|
||||
Action may have label or icon or both.
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@ from Qt import QtWidgets, QtCore, QtGui
|
|||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
||||
from openpype.tools import resources
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
from .models import (
|
||||
AssetsHierarchyModel,
|
||||
TasksModel,
|
||||
RecursiveSortFilterProxyModel,
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
IconButton,
|
||||
PixmapLabel,
|
||||
BaseClickableFrame
|
||||
)
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
from .assets_widget import AssetsDialog
|
||||
from .tasks_widget import TasksModel
|
||||
from .icons import (
|
||||
get_pixmap,
|
||||
get_icon_path
|
||||
|
|
@ -26,49 +29,14 @@ from ..constants import (
|
|||
)
|
||||
|
||||
|
||||
class PixmapLabel(QtWidgets.QLabel):
|
||||
"""Label resizing image to height of font."""
|
||||
def __init__(self, pixmap, parent):
|
||||
super(PixmapLabel, self).__init__(parent)
|
||||
self._source_pixmap = pixmap
|
||||
|
||||
def set_source_pixmap(self, pixmap):
|
||||
"""Change source image."""
|
||||
self._source_pixmap = pixmap
|
||||
self._set_resized_pix()
|
||||
|
||||
def _set_resized_pix(self):
|
||||
class PublishPixmapLabel(PixmapLabel):
|
||||
def _get_pix_size(self):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
self.setPixmap(
|
||||
self._source_pixmap.scaled(
|
||||
size,
|
||||
size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
super(PixmapLabel, self).resizeEvent(event)
|
||||
return size, size
|
||||
|
||||
|
||||
class TransparentPixmapLabel(QtWidgets.QLabel):
|
||||
"""Transparent label resizing to width and height of font."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TransparentPixmapLabel, self).__init__(*args, **kwargs)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
pix = QtGui.QPixmap(size, size)
|
||||
pix.fill(QtCore.Qt.transparent)
|
||||
self.setPixmap(pix)
|
||||
super(TransparentPixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class IconValuePixmapLabel(PixmapLabel):
|
||||
class IconValuePixmapLabel(PublishPixmapLabel):
|
||||
"""Label resizing to width and height of font.
|
||||
|
||||
Handle icon parsing from creators/instances. Using of QAwesome module
|
||||
|
|
@ -125,7 +93,7 @@ class IconValuePixmapLabel(PixmapLabel):
|
|||
return self._default_pixmap()
|
||||
|
||||
|
||||
class ContextWarningLabel(PixmapLabel):
|
||||
class ContextWarningLabel(PublishPixmapLabel):
|
||||
"""Pixmap label with warning icon."""
|
||||
def __init__(self, parent):
|
||||
pix = get_pixmap("warning")
|
||||
|
|
@ -138,29 +106,6 @@ class ContextWarningLabel(PixmapLabel):
|
|||
self.setObjectName("FamilyIconLabel")
|
||||
|
||||
|
||||
class IconButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
Using font metrics height as icon size reference.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IconButton, self).__init__(*args, **kwargs)
|
||||
self.setObjectName("IconButton")
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(IconButton, self).sizeHint()
|
||||
icon_h = self.iconSize().height()
|
||||
font_height = self.fontMetrics().height()
|
||||
text_set = bool(self.text())
|
||||
if not text_set and icon_h < font_height:
|
||||
new_size = result.height() - icon_h + font_height
|
||||
result.setHeight(new_size)
|
||||
result.setWidth(new_size)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PublishIconBtn(IconButton):
|
||||
"""Button using alpha of source image to redraw with different color.
|
||||
|
||||
|
|
@ -314,7 +259,7 @@ class ShowPublishReportBtn(PublishIconBtn):
|
|||
class RemoveInstanceBtn(PublishIconBtn):
|
||||
"""Create remove button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("delete")
|
||||
icon_path = resources.get_icon_path("delete")
|
||||
super(RemoveInstanceBtn, self).__init__(icon_path, parent)
|
||||
self.setToolTip("Remove selected instances")
|
||||
|
||||
|
|
@ -359,170 +304,6 @@ class AbstractInstanceView(QtWidgets.QWidget):
|
|||
).format(self.__class__.__name__))
|
||||
|
||||
|
||||
class ClickableFrame(QtWidgets.QFrame):
|
||||
"""Widget that catch left mouse click and can trigger a callback.
|
||||
|
||||
Callback is defined by overriding `_mouse_release_callback`.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(ClickableFrame, self).__init__(parent)
|
||||
|
||||
self._mouse_pressed = False
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
pass
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableFrame, self).mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self._mouse_release_callback()
|
||||
|
||||
super(ClickableFrame, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class AssetsDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select asset for a context of instance."""
|
||||
def __init__(self, controller, parent):
|
||||
super(AssetsDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Select asset")
|
||||
|
||||
model = AssetsHierarchyModel(controller)
|
||||
proxy_model = RecursiveSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
filter_input.setPlaceholderText("Filter assets..")
|
||||
|
||||
asset_view = QtWidgets.QTreeView(self)
|
||||
asset_view.setModel(proxy_model)
|
||||
asset_view.setHeaderHidden(True)
|
||||
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
asset_view.setAlternatingRowColors(True)
|
||||
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
|
||||
asset_view.setAllColumnsShowFocus(True)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(filter_input, 0)
|
||||
layout.addWidget(asset_view, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
filter_input.textChanged.connect(self._on_filter_change)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._filter_input = filter_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._asset_view = asset_view
|
||||
|
||||
self._selected_asset = None
|
||||
# Soft refresh is enabled
|
||||
# - reset will happen at all cost if soft reset is enabled
|
||||
# - adds ability to call reset on multiple places without repeating
|
||||
self._soft_reset_enabled = True
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Refresh asset model on show."""
|
||||
super(AssetsDialog, self).showEvent(event)
|
||||
# Refresh on show
|
||||
self.reset(False)
|
||||
|
||||
def reset(self, force=True):
|
||||
"""Reset asset model."""
|
||||
if not force and not self._soft_reset_enabled:
|
||||
return
|
||||
|
||||
if self._soft_reset_enabled:
|
||||
self._soft_reset_enabled = False
|
||||
|
||||
self._model.reset()
|
||||
|
||||
def name_is_valid(self, name):
|
||||
"""Is asset name valid.
|
||||
|
||||
Args:
|
||||
name(str): Asset name that should be checked.
|
||||
"""
|
||||
# Make sure we're reset
|
||||
self.reset(False)
|
||||
# Valid the name by model
|
||||
return self._model.name_is_valid(name)
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of assets."""
|
||||
self._proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self.done(0)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
index = self._asset_view.currentIndex()
|
||||
asset_name = None
|
||||
if index.isValid():
|
||||
asset_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._selected_asset = asset_name
|
||||
self.done(1)
|
||||
|
||||
def set_selected_assets(self, asset_names):
|
||||
"""Change preselected asset before showing the dialog.
|
||||
|
||||
This also resets model and clean filter.
|
||||
"""
|
||||
self.reset(False)
|
||||
self._asset_view.collapseAll()
|
||||
self._filter_input.setText("")
|
||||
|
||||
indexes = []
|
||||
for asset_name in asset_names:
|
||||
index = self._model.get_index_by_name(asset_name)
|
||||
if index.isValid():
|
||||
indexes.append(index)
|
||||
|
||||
if not indexes:
|
||||
return
|
||||
|
||||
index_deque = collections.deque()
|
||||
for index in indexes:
|
||||
index_deque.append(index)
|
||||
|
||||
all_indexes = []
|
||||
while index_deque:
|
||||
index = index_deque.popleft()
|
||||
all_indexes.append(index)
|
||||
|
||||
parent_index = index.parent()
|
||||
if parent_index.isValid():
|
||||
index_deque.append(parent_index)
|
||||
|
||||
for index in all_indexes:
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._asset_view.expand(proxy_index)
|
||||
|
||||
def get_selected_asset(self):
|
||||
"""Get selected asset name."""
|
||||
return self._selected_asset
|
||||
|
||||
|
||||
class ClickableLineEdit(QtWidgets.QLineEdit):
|
||||
"""QLineEdit capturing left mouse click.
|
||||
|
||||
|
|
@ -554,7 +335,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
event.accept()
|
||||
|
||||
|
||||
class AssetsField(ClickableFrame):
|
||||
class AssetsField(BaseClickableFrame):
|
||||
"""Field where asset name of selected instance/s is showed.
|
||||
|
||||
Click on the field will trigger `AssetsDialog`.
|
||||
|
|
@ -1394,12 +1175,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout = QtWidgets.QFormLayout(content_widget)
|
||||
for attr_def, attr_instances, values in result:
|
||||
widget = create_widget_for_attr_def(attr_def, content_widget)
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
widget.set_value(values[0])
|
||||
else:
|
||||
widget.set_value(values, True)
|
||||
if attr_def.is_value_def:
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
widget.set_value(values[0])
|
||||
else:
|
||||
widget.set_value(values, True)
|
||||
|
||||
label = attr_def.label or attr_def.key
|
||||
content_layout.addRow(label, widget)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ from openpype import (
|
|||
resources,
|
||||
style
|
||||
)
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
PixmapLabel
|
||||
)
|
||||
from .control import PublisherController
|
||||
from .widgets import (
|
||||
BorderedLabelWidget,
|
||||
|
|
@ -14,8 +17,6 @@ from .widgets import (
|
|||
InstanceListView,
|
||||
CreateDialog,
|
||||
|
||||
PixmapLabel,
|
||||
|
||||
StopBtn,
|
||||
ResetBtn,
|
||||
ValidateBtn,
|
||||
|
|
@ -32,7 +33,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
default_width = 1000
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, reset_on_show=None):
|
||||
super(PublisherWindow, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("OpenPype publisher")
|
||||
|
|
@ -40,6 +41,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
if reset_on_show is None:
|
||||
reset_on_show = True
|
||||
|
||||
if parent is None:
|
||||
on_top_flag = QtCore.Qt.WindowStaysOnTopHint
|
||||
else:
|
||||
|
|
@ -54,6 +58,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
| on_top_flag
|
||||
)
|
||||
|
||||
self._reset_on_show = reset_on_show
|
||||
self._first_show = True
|
||||
self._refreshing_instances = False
|
||||
|
||||
|
|
@ -116,12 +121,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
subset_view_btns_layout.addWidget(change_view_btn)
|
||||
|
||||
# Layout of view and buttons
|
||||
subset_view_layout = QtWidgets.QVBoxLayout()
|
||||
# - widget 'subset_view_widget' is necessary
|
||||
# - only layout won't be resized automatically to minimum size hint
|
||||
# on child resize request!
|
||||
subset_view_widget = QtWidgets.QWidget(subset_views_widget)
|
||||
subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)
|
||||
subset_view_layout.setContentsMargins(0, 0, 0, 0)
|
||||
subset_view_layout.addLayout(subset_views_layout, 1)
|
||||
subset_view_layout.addLayout(subset_view_btns_layout, 0)
|
||||
|
||||
subset_views_widget.set_center_widget(subset_view_layout)
|
||||
subset_views_widget.set_center_widget(subset_view_widget)
|
||||
|
||||
# Whole subset layout with attributes and details
|
||||
subset_content_widget = QtWidgets.QWidget(subset_frame)
|
||||
|
|
@ -248,7 +257,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._first_show = False
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.reset()
|
||||
if self._reset_on_show:
|
||||
self.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.controller.save_changes()
|
||||
|
|
@ -381,6 +391,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
context_title = self.controller.get_context_title()
|
||||
self.set_context_label(context_title)
|
||||
|
||||
# Give a change to process Resize Request
|
||||
QtWidgets.QApplication.processEvents()
|
||||
# Trigger update geometry of
|
||||
widget = self.subset_views_layout.currentWidget()
|
||||
widget.updateGeometry()
|
||||
|
||||
def _on_subset_change(self, *_args):
|
||||
# Ignore changes if in middle of refreshing
|
||||
if self._refreshing_instances:
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ def install_fonts():
|
|||
# In hosts, this will be called each time the GUI is shown,
|
||||
# potentially installing a font each time.
|
||||
if database.addApplicationFont(path) < 0:
|
||||
print("Could not install %s\n" % path)
|
||||
print("Could not install %s" % path)
|
||||
else:
|
||||
print("Installed %s\n" % font)
|
||||
print("Installed %s" % font)
|
||||
|
||||
|
||||
def on_destroyed():
|
||||
|
|
|
|||
|
|
@ -47,18 +47,22 @@ class MainThreadProcess(QtCore.QObject):
|
|||
|
||||
This approach gives ability to update UI meanwhile plugin is in progress.
|
||||
"""
|
||||
timer_interval = 3
|
||||
# How many times let pass QtApplication to process events
|
||||
# - use 2 as resize event can trigger repaint event but not process in
|
||||
# same loop
|
||||
count_timeout = 2
|
||||
|
||||
def __init__(self):
|
||||
super(MainThreadProcess, self).__init__()
|
||||
self._items_to_process = collections.deque()
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.setInterval(self.timer_interval)
|
||||
timer.setInterval(0)
|
||||
|
||||
timer.timeout.connect(self._execute)
|
||||
|
||||
self._timer = timer
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
def process(self, func, *args, **kwargs):
|
||||
item = MainThreadItem(func, *args, **kwargs)
|
||||
|
|
@ -71,6 +75,12 @@ class MainThreadProcess(QtCore.QObject):
|
|||
if not self._items_to_process:
|
||||
return
|
||||
|
||||
if self._switch_counter > 0:
|
||||
self._switch_counter -= 1
|
||||
return
|
||||
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
item = self._items_to_process.popleft()
|
||||
item.process()
|
||||
|
||||
|
|
|
|||
|
|
@ -25,3 +25,6 @@ TerminalFilters = {
|
|||
|
||||
# Allow animations in GUI
|
||||
Animated = env_variable_to_bool("OPENPYPE_PYBLISH_ANIMATED", True)
|
||||
|
||||
# Print UI info message to console
|
||||
PrintInfo = env_variable_to_bool("OPENPYPE_PYBLISH_PRINT_INFO", True)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class OverviewView(QtWidgets.QTreeView):
|
|||
toggled = QtCore.Signal(QtCore.QModelIndex, object)
|
||||
show_perspective = QtCore.Signal(QtCore.QModelIndex)
|
||||
|
||||
def __init__(self, animated, parent=None):
|
||||
def __init__(self, parent=None):
|
||||
super(OverviewView, self).__init__(parent)
|
||||
|
||||
self.horizontalScrollBar().hide()
|
||||
|
|
@ -28,8 +28,6 @@ class OverviewView(QtWidgets.QTreeView):
|
|||
self.setHeaderHidden(True)
|
||||
self.setRootIsDecorated(False)
|
||||
self.setIndentation(0)
|
||||
if animated:
|
||||
self.setAnimated(True)
|
||||
|
||||
def event(self, event):
|
||||
if not event.type() == QtCore.QEvent.KeyPress:
|
||||
|
|
|
|||
|
|
@ -143,9 +143,8 @@ class Window(QtWidgets.QDialog):
|
|||
# TODO add parent
|
||||
overview_page = QtWidgets.QWidget()
|
||||
|
||||
overview_instance_view = view.InstanceView(
|
||||
animated=settings.Animated, parent=overview_page
|
||||
)
|
||||
overview_instance_view = view.InstanceView(parent=overview_page)
|
||||
overview_instance_view.setAnimated(settings.Animated)
|
||||
overview_instance_delegate = delegate.InstanceDelegate(
|
||||
parent=overview_instance_view
|
||||
)
|
||||
|
|
@ -156,9 +155,8 @@ class Window(QtWidgets.QDialog):
|
|||
overview_instance_view.setItemDelegate(overview_instance_delegate)
|
||||
overview_instance_view.setModel(instance_sort_proxy)
|
||||
|
||||
overview_plugin_view = view.PluginView(
|
||||
animated=settings.Animated, parent=overview_page
|
||||
)
|
||||
overview_plugin_view = view.PluginView(parent=overview_page)
|
||||
overview_plugin_view.setAnimated(settings.Animated)
|
||||
overview_plugin_delegate = delegate.PluginDelegate(
|
||||
parent=overview_plugin_view
|
||||
)
|
||||
|
|
@ -307,11 +305,6 @@ class Window(QtWidgets.QDialog):
|
|||
on.setStartValue(0)
|
||||
on.setEndValue(1)
|
||||
|
||||
off = QtCore.QPropertyAnimation(info_effect, b"opacity")
|
||||
off.setDuration(0)
|
||||
off.setStartValue(1)
|
||||
off.setEndValue(0)
|
||||
|
||||
fade = QtCore.QPropertyAnimation(info_effect, b"opacity")
|
||||
fade.setDuration(500)
|
||||
fade.setStartValue(1.0)
|
||||
|
|
@ -319,10 +312,6 @@ class Window(QtWidgets.QDialog):
|
|||
|
||||
animation_info_msg = QtCore.QSequentialAnimationGroup()
|
||||
animation_info_msg.addAnimation(on)
|
||||
animation_info_msg.addPause(50)
|
||||
animation_info_msg.addAnimation(off)
|
||||
animation_info_msg.addPause(50)
|
||||
animation_info_msg.addAnimation(on)
|
||||
animation_info_msg.addPause(2000)
|
||||
animation_info_msg.addAnimation(fade)
|
||||
|
||||
|
|
@ -1023,9 +1012,11 @@ class Window(QtWidgets.QDialog):
|
|||
{GroupStates.HasFinished: True},
|
||||
Roles.PublishFlagsRole
|
||||
)
|
||||
self.overview_plugin_view.setAnimated(False)
|
||||
self.overview_plugin_view.collapse(group_index)
|
||||
|
||||
def on_was_stopped(self):
|
||||
self.overview_plugin_view.setAnimated(settings.Animated)
|
||||
errored = self.controller.errored
|
||||
if self.controller.collect_state == 0:
|
||||
self.footer_button_play.setEnabled(False)
|
||||
|
|
@ -1048,6 +1039,8 @@ class Window(QtWidgets.QDialog):
|
|||
and not self.controller.stopped
|
||||
)
|
||||
self.button_suspend_logs.setEnabled(suspend_log_bool)
|
||||
if not self.isVisible():
|
||||
self.setVisible(True)
|
||||
|
||||
def on_was_skipped(self, plugin):
|
||||
plugin_item = self.plugin_model.plugin_items[plugin.id]
|
||||
|
|
@ -1057,6 +1050,7 @@ class Window(QtWidgets.QDialog):
|
|||
)
|
||||
|
||||
def on_was_finished(self):
|
||||
self.overview_plugin_view.setAnimated(settings.Animated)
|
||||
self.footer_button_play.setEnabled(False)
|
||||
self.footer_button_validate.setEnabled(False)
|
||||
self.footer_button_reset.setEnabled(True)
|
||||
|
|
@ -1120,6 +1114,9 @@ class Window(QtWidgets.QDialog):
|
|||
plugin_item, instance_item
|
||||
)
|
||||
|
||||
if not self.isVisible():
|
||||
self.setVisible(True)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
#
|
||||
# Functions
|
||||
|
|
@ -1313,9 +1310,9 @@ class Window(QtWidgets.QDialog):
|
|||
self.animation_info_msg.stop()
|
||||
self.animation_info_msg.start()
|
||||
|
||||
# TODO(marcus): Should this be configurable? Do we want
|
||||
# the shell to fill up with these messages?
|
||||
util.u_print(message)
|
||||
if settings.PrintInfo:
|
||||
# Print message to console
|
||||
util.u_print(message)
|
||||
|
||||
def warning(self, message):
|
||||
"""Block processing and print warning until user hits "Continue"
|
||||
|
|
|
|||
45
openpype/tools/resources/__init__.py
Normal file
45
openpype/tools/resources/__init__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
|
||||
from Qt import QtGui
|
||||
|
||||
|
||||
def get_icon_path(icon_name=None, filename=None):
|
||||
"""Path to image in './images' folder."""
|
||||
if icon_name is None and filename is None:
|
||||
return None
|
||||
|
||||
if filename is None:
|
||||
filename = "{}.png".format(icon_name)
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"images",
|
||||
filename
|
||||
)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def get_image(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QImage."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QImage(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_pixmap(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QPixmap."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QPixmap(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_icon(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QICon."""
|
||||
pix = get_pixmap(icon_name, filename)
|
||||
if pix:
|
||||
return QtGui.QIcon(pix)
|
||||
return None
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
openpype/tools/resources/images/file.png
Normal file
BIN
openpype/tools/resources/images/file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
openpype/tools/resources/images/files.png
Normal file
BIN
openpype/tools/resources/images/files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
BIN
openpype/tools/resources/images/folder.png
Normal file
BIN
openpype/tools/resources/images/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -11,15 +11,12 @@ from openpype.tools.utils import PlaceholderLineEdit
|
|||
class AppVariantWidget(QtWidgets.QWidget):
|
||||
exec_placeholder = "< Specific path for this machine >"
|
||||
|
||||
def __init__(self, group_label, variant_name, variant_entity, parent):
|
||||
def __init__(
|
||||
self, group_label, variant_name, variant_label, variant_entity, parent
|
||||
):
|
||||
super(AppVariantWidget, self).__init__(parent)
|
||||
|
||||
self.executable_input_widget = None
|
||||
variant_label = variant_entity.label
|
||||
if variant_label is None:
|
||||
parent_entity = variant_entity.parent
|
||||
if hasattr(parent_entity, "get_key_label"):
|
||||
variant_label = parent_entity.get_key_label(variant_name)
|
||||
|
||||
if not variant_label:
|
||||
variant_label = variant_name
|
||||
|
|
@ -107,15 +104,15 @@ class AppVariantWidget(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class AppGroupWidget(QtWidgets.QWidget):
|
||||
def __init__(self, group_entity, parent):
|
||||
def __init__(self, group_entity, group_label, parent, dynamic=False):
|
||||
super(AppGroupWidget, self).__init__(parent)
|
||||
|
||||
variants_entity = group_entity["variants"]
|
||||
valid_variants = {}
|
||||
for key, entity in group_entity["variants"].items():
|
||||
for key, entity in variants_entity.items():
|
||||
if "enabled" not in entity or entity["enabled"].value:
|
||||
valid_variants[key] = entity
|
||||
|
||||
group_label = group_entity.label
|
||||
expading_widget = ExpandingWidget(group_label, self)
|
||||
content_widget = QtWidgets.QWidget(expading_widget)
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
|
|
@ -126,8 +123,16 @@ class AppGroupWidget(QtWidgets.QWidget):
|
|||
if "executables" not in variant_entity:
|
||||
continue
|
||||
|
||||
variant_label = variant_entity.label
|
||||
if dynamic and hasattr(variants_entity, "get_key_label"):
|
||||
variant_label = variants_entity.get_key_label(variant_name)
|
||||
|
||||
variant_widget = AppVariantWidget(
|
||||
group_label, variant_name, variant_entity, content_widget
|
||||
group_label,
|
||||
variant_name,
|
||||
variant_label,
|
||||
variant_entity,
|
||||
content_widget
|
||||
)
|
||||
widgets_by_variant_name[variant_name] = variant_widget
|
||||
content_layout.addWidget(variant_widget)
|
||||
|
|
@ -171,6 +176,20 @@ class LocalApplicationsWidgets(QtWidgets.QWidget):
|
|||
|
||||
self.content_layout = layout
|
||||
|
||||
def _filter_group_entity(self, entity):
|
||||
if not entity["enabled"].value:
|
||||
return False
|
||||
|
||||
# Check if has enabled any variant
|
||||
for variant_entity in entity["variants"].values():
|
||||
if (
|
||||
"enabled" not in variant_entity
|
||||
or variant_entity["enabled"].value
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _reset_app_widgets(self):
|
||||
while self.content_layout.count() > 0:
|
||||
item = self.content_layout.itemAt(0)
|
||||
|
|
@ -180,26 +199,35 @@ class LocalApplicationsWidgets(QtWidgets.QWidget):
|
|||
self.content_layout.removeItem(item)
|
||||
self.widgets_by_group_name.clear()
|
||||
|
||||
app_items = {}
|
||||
additional_apps = set()
|
||||
additional_apps_entity = None
|
||||
for key, entity in self.system_settings_entity["applications"].items():
|
||||
# Filter not enabled app groups
|
||||
if not entity["enabled"].value:
|
||||
if key != "additional_apps":
|
||||
app_items[key] = entity
|
||||
continue
|
||||
|
||||
# Check if has enabled any variant
|
||||
enabled_variant = False
|
||||
for variant_entity in entity["variants"].values():
|
||||
if (
|
||||
"enabled" not in variant_entity
|
||||
or variant_entity["enabled"].value
|
||||
):
|
||||
enabled_variant = True
|
||||
break
|
||||
additional_apps_entity = entity
|
||||
for _key, _entity in entity.items():
|
||||
app_items[_key] = _entity
|
||||
additional_apps.add(_key)
|
||||
|
||||
if not enabled_variant:
|
||||
for key, entity in app_items.items():
|
||||
if not self._filter_group_entity(entity):
|
||||
continue
|
||||
|
||||
dynamic = key in additional_apps
|
||||
group_label = None
|
||||
if dynamic and hasattr(additional_apps_entity, "get_key_label"):
|
||||
group_label = additional_apps_entity.get_key_label(key)
|
||||
|
||||
if not group_label:
|
||||
group_label = entity.label
|
||||
if not group_label:
|
||||
group_label = key
|
||||
|
||||
# Create App group specific widget and store it by the key
|
||||
group_widget = AppGroupWidget(entity, self)
|
||||
group_widget = AppGroupWidget(entity, group_label, self, dynamic)
|
||||
if group_widget.widgets_by_variant_name:
|
||||
self.widgets_by_group_name[key] = group_widget
|
||||
self.content_layout.addWidget(group_widget)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from openpype.settings.lib import (
|
|||
)
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
SystemSettings,
|
||||
ProjectSettings
|
||||
)
|
||||
|
|
@ -32,7 +33,7 @@ from .constants import (
|
|||
LOCAL_APPS_KEY
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class LocalSettingsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -250,6 +251,9 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
self._settings_widget.update_local_settings(value)
|
||||
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"Failed to create local settings window", exc_info=True
|
||||
)
|
||||
error_msg = str(exc)
|
||||
|
||||
crashed = error_msg is not None
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import sys
|
|||
import traceback
|
||||
import contextlib
|
||||
from enum import Enum
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.lib import get_openpype_version
|
||||
from openpype.tools.utils import set_style_property
|
||||
from openpype.settings.entities import (
|
||||
SystemSettings,
|
||||
ProjectSettings,
|
||||
|
|
@ -34,7 +36,10 @@ from openpype.settings.entities.op_version_entity import (
|
|||
)
|
||||
|
||||
from openpype.settings import SaveWarningExc
|
||||
from .widgets import ProjectListWidget
|
||||
from .widgets import (
|
||||
ProjectListWidget,
|
||||
VersionAction
|
||||
)
|
||||
from .breadcrumbs_widget import (
|
||||
BreadcrumbsAddressBar,
|
||||
SystemSettingsBreadcrumbs,
|
||||
|
|
@ -90,6 +95,20 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
reset_finished = QtCore.Signal()
|
||||
full_path_requested = QtCore.Signal(str, str)
|
||||
|
||||
require_restart_label_text = (
|
||||
"Your changes require restart of"
|
||||
" all running OpenPype processes to take affect."
|
||||
)
|
||||
outdated_version_label_text = (
|
||||
"Your settings are loaded from an older version."
|
||||
)
|
||||
source_version_tooltip = "Using settings of current OpenPype version"
|
||||
source_version_tooltip_outdated = (
|
||||
"Please check that all settings are still correct (blue colour\n"
|
||||
"indicates potential changes in the new version) and save your\n"
|
||||
"settings to update them to you current running OpenPype version."
|
||||
)
|
||||
|
||||
def __init__(self, user_role, parent=None):
|
||||
super(SettingsCategoryWidget, self).__init__(parent)
|
||||
|
||||
|
|
@ -100,6 +119,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
self._state = CategoryState.Idle
|
||||
|
||||
self._hide_studio_overrides = False
|
||||
self._updating_root = False
|
||||
self._use_version = None
|
||||
self._current_version = get_openpype_version()
|
||||
|
||||
self.ignore_input_changes = IgnoreInputChangesObj(self)
|
||||
|
||||
self.keys = []
|
||||
|
|
@ -185,77 +208,126 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
def initialize_attributes(self):
|
||||
return
|
||||
|
||||
@property
|
||||
def is_modifying_defaults(self):
|
||||
if self.modify_defaults_checkbox is None:
|
||||
return False
|
||||
return self.modify_defaults_checkbox.isChecked()
|
||||
|
||||
def create_ui(self):
|
||||
self.modify_defaults_checkbox = None
|
||||
|
||||
scroll_widget = QtWidgets.QScrollArea(self)
|
||||
scroll_widget.setObjectName("GroupWidget")
|
||||
content_widget = QtWidgets.QWidget(scroll_widget)
|
||||
conf_wrapper_widget = QtWidgets.QWidget(self)
|
||||
configurations_widget = QtWidgets.QWidget(conf_wrapper_widget)
|
||||
|
||||
breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget)
|
||||
breadcrumbs_widget = BreadcrumbsAddressBar(content_widget)
|
||||
# Breadcrumbs/Path widget
|
||||
breadcrumbs_widget = QtWidgets.QWidget(self)
|
||||
breadcrumbs_label = QtWidgets.QLabel("Path:", breadcrumbs_widget)
|
||||
breadcrumbs_bar = BreadcrumbsAddressBar(breadcrumbs_widget)
|
||||
|
||||
breadcrumbs_layout = QtWidgets.QHBoxLayout()
|
||||
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(breadcrumbs_widget)
|
||||
refresh_btn.setIcon(refresh_icon)
|
||||
|
||||
breadcrumbs_layout = QtWidgets.QHBoxLayout(breadcrumbs_widget)
|
||||
breadcrumbs_layout.setContentsMargins(5, 5, 5, 5)
|
||||
breadcrumbs_layout.setSpacing(5)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_label)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_widget)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_label, 0)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_bar, 1)
|
||||
breadcrumbs_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
# Widgets representing settings entities
|
||||
scroll_widget = QtWidgets.QScrollArea(configurations_widget)
|
||||
content_widget = QtWidgets.QWidget(scroll_widget)
|
||||
scroll_widget.setWidgetResizable(True)
|
||||
scroll_widget.setWidget(content_widget)
|
||||
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(3, 3, 3, 3)
|
||||
content_layout.setSpacing(5)
|
||||
content_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
scroll_widget.setWidgetResizable(True)
|
||||
scroll_widget.setWidget(content_widget)
|
||||
# Footer widget
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
footer_widget.setObjectName("SettingsFooter")
|
||||
|
||||
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(self)
|
||||
refresh_btn.setIcon(refresh_icon)
|
||||
# Info labels
|
||||
# TODO dynamic labels
|
||||
labels_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
|
||||
empty_label = QtWidgets.QLabel(footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout()
|
||||
outdated_version_label = QtWidgets.QLabel(
|
||||
self.outdated_version_label_text, footer_widget
|
||||
)
|
||||
outdated_version_label.setToolTip(self.source_version_tooltip_outdated)
|
||||
outdated_version_label.setAlignment(labels_alignment)
|
||||
outdated_version_label.setVisible(False)
|
||||
outdated_version_label.setObjectName("SettingsOutdatedSourceVersion")
|
||||
|
||||
require_restart_label = QtWidgets.QLabel(
|
||||
self.require_restart_label_text, footer_widget
|
||||
)
|
||||
require_restart_label.setAlignment(labels_alignment)
|
||||
require_restart_label.setVisible(False)
|
||||
|
||||
# Label showing source version of loaded settings
|
||||
source_version_label = QtWidgets.QLabel("", footer_widget)
|
||||
source_version_label.setObjectName("SourceVersionLabel")
|
||||
set_style_property(source_version_label, "state", "")
|
||||
source_version_label.setToolTip(self.source_version_tooltip)
|
||||
|
||||
save_btn = QtWidgets.QPushButton("Save", footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(5, 5, 5, 5)
|
||||
if self.user_role == "developer":
|
||||
self._add_developer_ui(footer_layout)
|
||||
self._add_developer_ui(footer_layout, footer_widget)
|
||||
|
||||
save_btn = QtWidgets.QPushButton("Save", self)
|
||||
require_restart_label = QtWidgets.QLabel(self)
|
||||
require_restart_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
footer_layout.addWidget(refresh_btn, 0)
|
||||
footer_layout.addWidget(empty_label, 1)
|
||||
footer_layout.addWidget(outdated_version_label, 1)
|
||||
footer_layout.addWidget(require_restart_label, 1)
|
||||
footer_layout.addWidget(source_version_label, 0)
|
||||
footer_layout.addWidget(save_btn, 0)
|
||||
|
||||
configurations_layout = QtWidgets.QVBoxLayout()
|
||||
configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
|
||||
configurations_layout.setContentsMargins(0, 0, 0, 0)
|
||||
configurations_layout.setSpacing(0)
|
||||
|
||||
configurations_layout.addWidget(scroll_widget, 1)
|
||||
configurations_layout.addLayout(footer_layout, 0)
|
||||
|
||||
conf_wrapper_layout = QtWidgets.QHBoxLayout()
|
||||
conf_wrapper_layout = QtWidgets.QHBoxLayout(conf_wrapper_widget)
|
||||
conf_wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
conf_wrapper_layout.setSpacing(0)
|
||||
conf_wrapper_layout.addLayout(configurations_layout, 1)
|
||||
conf_wrapper_layout.addWidget(configurations_widget, 1)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
main_layout.addLayout(breadcrumbs_layout, 0)
|
||||
main_layout.addLayout(conf_wrapper_layout, 1)
|
||||
main_layout.addWidget(breadcrumbs_widget, 0)
|
||||
main_layout.addWidget(conf_wrapper_widget, 1)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
save_btn.clicked.connect(self._save)
|
||||
refresh_btn.clicked.connect(self._on_refresh)
|
||||
breadcrumbs_widget.path_edited.connect(self._on_path_edit)
|
||||
breadcrumbs_bar.path_edited.connect(self._on_path_edit)
|
||||
|
||||
self._require_restart_label = require_restart_label
|
||||
self._outdated_version_label = outdated_version_label
|
||||
self._empty_label = empty_label
|
||||
|
||||
self._is_loaded_version_outdated = False
|
||||
|
||||
self.save_btn = save_btn
|
||||
self.refresh_btn = refresh_btn
|
||||
self.require_restart_label = require_restart_label
|
||||
self._source_version_label = source_version_label
|
||||
|
||||
self.scroll_widget = scroll_widget
|
||||
self.content_layout = content_layout
|
||||
self.content_widget = content_widget
|
||||
self.breadcrumbs_widget = breadcrumbs_widget
|
||||
self.breadcrumbs_bar = breadcrumbs_bar
|
||||
|
||||
self.breadcrumbs_model = None
|
||||
self.refresh_btn = refresh_btn
|
||||
|
||||
self.conf_wrapper_layout = conf_wrapper_layout
|
||||
self.main_layout = main_layout
|
||||
|
||||
|
|
@ -315,21 +387,17 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_path(self, path):
|
||||
"""Called from clicked widget."""
|
||||
self.breadcrumbs_widget.set_path(path)
|
||||
self.breadcrumbs_bar.set_path(path)
|
||||
|
||||
def _add_developer_ui(self, footer_layout):
|
||||
modify_defaults_widget = QtWidgets.QWidget()
|
||||
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
|
||||
def _add_developer_ui(self, footer_layout, footer_widget):
|
||||
modify_defaults_checkbox = QtWidgets.QCheckBox(footer_widget)
|
||||
modify_defaults_checkbox.setChecked(self._hide_studio_overrides)
|
||||
label_widget = QtWidgets.QLabel(
|
||||
"Modify defaults", modify_defaults_widget
|
||||
"Modify defaults", footer_widget
|
||||
)
|
||||
|
||||
modify_defaults_layout = QtWidgets.QHBoxLayout(modify_defaults_widget)
|
||||
modify_defaults_layout.addWidget(label_widget)
|
||||
modify_defaults_layout.addWidget(modify_defaults_checkbox)
|
||||
|
||||
footer_layout.addWidget(modify_defaults_widget, 0)
|
||||
footer_layout.addWidget(label_widget, 0)
|
||||
footer_layout.addWidget(modify_defaults_checkbox, 0)
|
||||
|
||||
modify_defaults_checkbox.stateChanged.connect(
|
||||
self._on_modify_defaults
|
||||
|
|
@ -368,6 +436,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
try:
|
||||
self.entity.save()
|
||||
self._use_version = None
|
||||
|
||||
# NOTE There are relations to previous entities and C++ callbacks
|
||||
# so it is easier to just use new entity and recreate UI but
|
||||
|
|
@ -427,13 +496,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
def _on_require_restart_change(self):
|
||||
value = ""
|
||||
if self.entity.require_restart:
|
||||
value = (
|
||||
"Your changes require restart of"
|
||||
" all running OpenPype processes to take affect."
|
||||
)
|
||||
self.require_restart_label.setText(value)
|
||||
self._update_labels_visibility()
|
||||
|
||||
def reset(self):
|
||||
self.reset_started.emit()
|
||||
|
|
@ -452,6 +515,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
widget.deleteLater()
|
||||
|
||||
dialog = None
|
||||
self._updating_root = True
|
||||
source_version = ""
|
||||
try:
|
||||
self._create_root_entity()
|
||||
|
||||
|
|
@ -467,6 +532,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
input_field.set_entity_value()
|
||||
|
||||
self.ignore_input_changes.set_ignore(False)
|
||||
source_version = self.entity.source_version
|
||||
|
||||
except DefaultsNotDefined:
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
|
|
@ -510,6 +576,27 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
spacer, layout.rowCount(), 0, 1, layout.columnCount()
|
||||
)
|
||||
|
||||
self._updating_root = False
|
||||
|
||||
# Update source version label
|
||||
state_value = ""
|
||||
tooltip = ""
|
||||
outdated = False
|
||||
if source_version:
|
||||
if source_version != self._current_version:
|
||||
state_value = "different"
|
||||
tooltip = self.source_version_tooltip_outdated
|
||||
outdated = True
|
||||
else:
|
||||
state_value = "same"
|
||||
tooltip = self.source_version_tooltip
|
||||
|
||||
self._is_loaded_version_outdated = outdated
|
||||
self._source_version_label.setText(source_version)
|
||||
self._source_version_label.setToolTip(tooltip)
|
||||
set_style_property(self._source_version_label, "state", state_value)
|
||||
self._update_labels_visibility()
|
||||
|
||||
self.set_state(CategoryState.Idle)
|
||||
|
||||
if dialog:
|
||||
|
|
@ -519,6 +606,36 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
self._on_reset_success()
|
||||
self.reset_finished.emit()
|
||||
|
||||
def _on_source_version_change(self, version):
|
||||
if self._updating_root:
|
||||
return
|
||||
|
||||
if version == self._current_version:
|
||||
version = None
|
||||
|
||||
self._use_version = version
|
||||
QtCore.QTimer.singleShot(20, self.reset)
|
||||
|
||||
def add_context_actions(self, menu):
|
||||
if not self.entity or self.is_modifying_defaults:
|
||||
return
|
||||
|
||||
versions = self.entity.get_available_studio_versions(sorted=True)
|
||||
if not versions:
|
||||
return
|
||||
|
||||
submenu = QtWidgets.QMenu("Use settings from version", menu)
|
||||
for version in reversed(versions):
|
||||
action = VersionAction(version, submenu)
|
||||
action.version_triggered.connect(
|
||||
self._on_context_version_trigger
|
||||
)
|
||||
submenu.addAction(action)
|
||||
menu.addMenu(submenu)
|
||||
|
||||
def _on_context_version_trigger(self, version):
|
||||
self._on_source_version_change(version)
|
||||
|
||||
def _on_reset_crash(self):
|
||||
self.save_btn.setEnabled(False)
|
||||
|
||||
|
|
@ -530,10 +647,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
self.save_btn.setEnabled(True)
|
||||
|
||||
if self.breadcrumbs_model is not None:
|
||||
path = self.breadcrumbs_widget.path()
|
||||
self.breadcrumbs_widget.set_path("")
|
||||
path = self.breadcrumbs_bar.path()
|
||||
self.breadcrumbs_bar.set_path("")
|
||||
self.breadcrumbs_model.set_entity(self.entity)
|
||||
self.breadcrumbs_widget.change_path(path)
|
||||
self.breadcrumbs_bar.change_path(path)
|
||||
|
||||
def add_children_gui(self):
|
||||
for child_obj in self.entity.children:
|
||||
|
|
@ -574,10 +691,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
def _save(self):
|
||||
# Don't trigger restart if defaults are modified
|
||||
if (
|
||||
self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
if self.is_modifying_defaults:
|
||||
require_restart = False
|
||||
else:
|
||||
require_restart = self.entity.require_restart
|
||||
|
|
@ -593,7 +707,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
if require_restart:
|
||||
self.restart_required_trigger.emit()
|
||||
self.require_restart_label.setText("")
|
||||
|
||||
def _update_labels_visibility(self):
|
||||
visible_label = None
|
||||
labels = {
|
||||
self._empty_label,
|
||||
self._outdated_version_label,
|
||||
self._require_restart_label,
|
||||
}
|
||||
if self.entity.require_restart:
|
||||
visible_label = self._require_restart_label
|
||||
elif self._is_loaded_version_outdated:
|
||||
visible_label = self._outdated_version_label
|
||||
else:
|
||||
visible_label = self._empty_label
|
||||
|
||||
if visible_label.isVisible():
|
||||
return
|
||||
|
||||
for label in labels:
|
||||
if label is visible_label:
|
||||
visible_label.setVisible(True)
|
||||
else:
|
||||
label.setVisible(False)
|
||||
|
||||
def _on_refresh(self):
|
||||
self.reset()
|
||||
|
|
@ -603,25 +739,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class SystemWidget(SettingsCategoryWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._actions = []
|
||||
super(SystemWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def contain_category_key(self, category):
|
||||
if category == "system_settings":
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_category_path(self, category, path):
|
||||
self.breadcrumbs_widget.change_path(path)
|
||||
self.breadcrumbs_bar.change_path(path)
|
||||
|
||||
def _create_root_entity(self):
|
||||
self.entity = SystemSettings(set_studio_state=False)
|
||||
self.entity.on_change_callbacks.append(self._on_entity_change)
|
||||
entity = SystemSettings(
|
||||
set_studio_state=False, source_version=self._use_version
|
||||
)
|
||||
entity.on_change_callbacks.append(self._on_entity_change)
|
||||
self.entity = entity
|
||||
try:
|
||||
if (
|
||||
self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
self.entity.set_defaults_state()
|
||||
if self.is_modifying_defaults:
|
||||
entity.set_defaults_state()
|
||||
else:
|
||||
self.entity.set_studio_state()
|
||||
entity.set_studio_state()
|
||||
|
||||
if self.modify_defaults_checkbox:
|
||||
self.modify_defaults_checkbox.setEnabled(True)
|
||||
|
|
@ -629,16 +769,16 @@ class SystemWidget(SettingsCategoryWidget):
|
|||
if not self.modify_defaults_checkbox:
|
||||
raise
|
||||
|
||||
self.entity.set_defaults_state()
|
||||
entity.set_defaults_state()
|
||||
self.modify_defaults_checkbox.setChecked(True)
|
||||
self.modify_defaults_checkbox.setEnabled(False)
|
||||
|
||||
def ui_tweaks(self):
|
||||
self.breadcrumbs_model = SystemSettingsBreadcrumbs()
|
||||
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
|
||||
self.breadcrumbs_bar.set_model(self.breadcrumbs_model)
|
||||
|
||||
def _on_modify_defaults(self):
|
||||
if self.modify_defaults_checkbox.isChecked():
|
||||
if self.is_modifying_defaults:
|
||||
if not self.entity.is_in_defaults_state():
|
||||
self.reset()
|
||||
else:
|
||||
|
|
@ -647,6 +787,9 @@ class SystemWidget(SettingsCategoryWidget):
|
|||
|
||||
|
||||
class ProjectWidget(SettingsCategoryWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def contain_category_key(self, category):
|
||||
if category in ("project_settings", "project_anatomy"):
|
||||
return True
|
||||
|
|
@ -660,28 +803,28 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
else:
|
||||
path = category
|
||||
|
||||
self.breadcrumbs_widget.change_path(path)
|
||||
self.breadcrumbs_bar.change_path(path)
|
||||
|
||||
def initialize_attributes(self):
|
||||
self.project_name = None
|
||||
|
||||
def ui_tweaks(self):
|
||||
self.breadcrumbs_model = ProjectSettingsBreadcrumbs()
|
||||
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
|
||||
self.breadcrumbs_bar.set_model(self.breadcrumbs_model)
|
||||
|
||||
project_list_widget = ProjectListWidget(self)
|
||||
|
||||
self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0)
|
||||
|
||||
project_list_widget.project_changed.connect(self._on_project_change)
|
||||
project_list_widget.version_change_requested.connect(
|
||||
self._on_source_version_change
|
||||
)
|
||||
|
||||
self.project_list_widget = project_list_widget
|
||||
|
||||
def get_project_names(self):
|
||||
if (
|
||||
self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
if self.is_modifying_defaults:
|
||||
return []
|
||||
return self.project_list_widget.get_project_names()
|
||||
|
||||
|
|
@ -693,6 +836,10 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
if self is saved_tab_widget:
|
||||
return
|
||||
|
||||
def _on_context_version_trigger(self, version):
|
||||
self.project_list_widget.select_project(None)
|
||||
super(ProjectWidget, self)._on_context_version_trigger(version)
|
||||
|
||||
def _on_reset_start(self):
|
||||
self.project_list_widget.refresh()
|
||||
|
||||
|
|
@ -705,32 +852,29 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
super(ProjectWidget, self)._on_reset_success()
|
||||
|
||||
def _set_enabled_project_list(self, enabled):
|
||||
if (
|
||||
enabled
|
||||
and self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
if enabled and self.is_modifying_defaults:
|
||||
enabled = False
|
||||
if self.project_list_widget.isEnabled() != enabled:
|
||||
self.project_list_widget.setEnabled(enabled)
|
||||
|
||||
def _create_root_entity(self):
|
||||
self.entity = ProjectSettings(change_state=False)
|
||||
self.entity.on_change_callbacks.append(self._on_entity_change)
|
||||
entity = ProjectSettings(
|
||||
change_state=False, source_version=self._use_version
|
||||
)
|
||||
entity.on_change_callbacks.append(self._on_entity_change)
|
||||
self.project_list_widget.set_entity(entity)
|
||||
self.entity = entity
|
||||
try:
|
||||
if (
|
||||
self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
if self.is_modifying_defaults:
|
||||
self.entity.set_defaults_state()
|
||||
|
||||
elif self.project_name is None:
|
||||
self.entity.set_studio_state()
|
||||
|
||||
elif self.project_name == self.entity.project_name:
|
||||
self.entity.set_project_state()
|
||||
else:
|
||||
self.entity.change_project(self.project_name)
|
||||
self.entity.change_project(
|
||||
self.project_name, self._use_version
|
||||
)
|
||||
|
||||
if self.modify_defaults_checkbox:
|
||||
self.modify_defaults_checkbox.setEnabled(True)
|
||||
|
|
@ -763,7 +907,7 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
self.set_state(CategoryState.Idle)
|
||||
|
||||
def _on_modify_defaults(self):
|
||||
if self.modify_defaults_checkbox.isChecked():
|
||||
if self.is_modifying_defaults:
|
||||
self._set_enabled_project_list(False)
|
||||
if not self.entity.is_in_defaults_state():
|
||||
self.reset()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ DEFAULT_PROJECT_LABEL = "< Default >"
|
|||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2
|
||||
PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3
|
||||
PROJECT_VERSION_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -12,5 +13,6 @@ __all__ = (
|
|||
|
||||
"PROJECT_NAME_ROLE",
|
||||
"PROJECT_IS_ACTIVE_ROLE",
|
||||
"PROJECT_IS_SELECTED_ROLE"
|
||||
"PROJECT_IS_SELECTED_ROLE",
|
||||
"PROJECT_VERSION_ROLE",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import copy
|
||||
import uuid
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.mongodb import (
|
||||
|
|
@ -12,8 +13,12 @@ from openpype.tools.utils.widgets import ImageButton
|
|||
from openpype.tools.utils.lib import paint_image_with_color
|
||||
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.settings.lib import get_system_settings
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
DynamicQThread
|
||||
)
|
||||
from openpype.settings.lib import find_closest_version_for_projects
|
||||
from openpype.lib import get_openpype_version
|
||||
from .images import (
|
||||
get_pixmap,
|
||||
get_image
|
||||
|
|
@ -21,11 +26,40 @@ from .images import (
|
|||
from .constants import (
|
||||
DEFAULT_PROJECT_LABEL,
|
||||
PROJECT_NAME_ROLE,
|
||||
PROJECT_VERSION_ROLE,
|
||||
PROJECT_IS_ACTIVE_ROLE,
|
||||
PROJECT_IS_SELECTED_ROLE
|
||||
)
|
||||
|
||||
|
||||
class SettingsTabWidget(QtWidgets.QTabWidget):
|
||||
context_menu_requested = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SettingsTabWidget, self).__init__(*args, **kwargs)
|
||||
self._right_click_tab_idx = None
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
super(SettingsTabWidget, self).mousePressEvent(event)
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
tab_bar = self.tabBar()
|
||||
pos = tab_bar.mapFromGlobal(event.globalPos())
|
||||
tab_idx = tab_bar.tabAt(pos)
|
||||
if tab_idx < 0:
|
||||
tab_idx = None
|
||||
self._right_click_tab_idx = tab_idx
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
super(SettingsTabWidget, self).mouseReleaseEvent(event)
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
tab_bar = self.tabBar()
|
||||
pos = tab_bar.mapFromGlobal(event.globalPos())
|
||||
tab_idx = tab_bar.tabAt(pos)
|
||||
if tab_idx == self._right_click_tab_idx:
|
||||
self.context_menu_requested.emit(tab_idx)
|
||||
self._right_click_tab = None
|
||||
|
||||
|
||||
class CompleterFilter(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CompleterFilter, self).__init__(*args, **kwargs)
|
||||
|
|
@ -603,7 +637,7 @@ class UnsavedChangesDialog(QtWidgets.QDialog):
|
|||
message = "You have unsaved changes. What do you want to do with them?"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
super(UnsavedChangesDialog, self).__init__(parent)
|
||||
message_label = QtWidgets.QLabel(self.message)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -735,19 +769,65 @@ class SettingsNiceCheckbox(NiceCheckbox):
|
|||
|
||||
|
||||
class ProjectModel(QtGui.QStandardItemModel):
|
||||
_update_versions = QtCore.Signal()
|
||||
|
||||
def __init__(self, only_active, *args, **kwargs):
|
||||
super(ProjectModel, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setColumnCount(2)
|
||||
|
||||
self.dbcon = None
|
||||
|
||||
self._only_active = only_active
|
||||
self._default_item = None
|
||||
self._items_by_name = {}
|
||||
self._versions_by_project = {}
|
||||
|
||||
colors = get_objected_colors()
|
||||
font_color = colors["font"].get_qcolor()
|
||||
font_color.setAlpha(67)
|
||||
self._version_font_color = font_color
|
||||
self._current_version = get_openpype_version()
|
||||
|
||||
self._version_refresh_threads = []
|
||||
self._version_refresh_id = None
|
||||
|
||||
self._update_versions.connect(self._on_update_versions_signal)
|
||||
|
||||
def _on_update_versions_signal(self):
|
||||
for project_name, version in self._versions_by_project.items():
|
||||
if project_name is None:
|
||||
item = self._default_item
|
||||
else:
|
||||
item = self._items_by_name.get(project_name)
|
||||
|
||||
if item and version != self._current_version:
|
||||
item.setData(version, PROJECT_VERSION_ROLE)
|
||||
|
||||
def _fetch_settings_versions(self):
|
||||
"""Used versions per project are loaded in thread to not stuck UI."""
|
||||
version_refresh_id = self._version_refresh_id
|
||||
all_project_names = list(self._items_by_name.keys())
|
||||
all_project_names.append(None)
|
||||
closest_by_project_name = find_closest_version_for_projects(
|
||||
all_project_names
|
||||
)
|
||||
if self._version_refresh_id == version_refresh_id:
|
||||
self._versions_by_project = closest_by_project_name
|
||||
self._update_versions.emit()
|
||||
|
||||
def flags(self, index):
|
||||
if index.column() == 1:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(ProjectModel, self).flags(index)
|
||||
|
||||
def set_dbcon(self, dbcon):
|
||||
self.dbcon = dbcon
|
||||
|
||||
def refresh(self):
|
||||
# Change id of versions refresh
|
||||
self._version_refresh_id = uuid.uuid4()
|
||||
|
||||
new_items = []
|
||||
if self._default_item is None:
|
||||
item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL)
|
||||
|
|
@ -757,6 +837,7 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
new_items.append(item)
|
||||
self._default_item = item
|
||||
|
||||
self._default_item.setData("", PROJECT_VERSION_ROLE)
|
||||
project_names = set()
|
||||
if self.dbcon is not None:
|
||||
for project_doc in self.dbcon.projects(
|
||||
|
|
@ -776,6 +857,7 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
is_active = project_doc.get("data", {}).get("active", True)
|
||||
item.setData(project_name, PROJECT_NAME_ROLE)
|
||||
item.setData(is_active, PROJECT_IS_ACTIVE_ROLE)
|
||||
item.setData("", PROJECT_VERSION_ROLE)
|
||||
item.setData(False, PROJECT_IS_SELECTED_ROLE)
|
||||
|
||||
if not is_active:
|
||||
|
|
@ -792,15 +874,87 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
# Fetch versions per project in thread
|
||||
thread = DynamicQThread(self._fetch_settings_versions)
|
||||
self._version_refresh_threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
class ProjectListView(QtWidgets.QListView):
|
||||
# Cleanup done threads
|
||||
for thread in tuple(self._version_refresh_threads):
|
||||
if thread.isFinished():
|
||||
self._version_refresh_threads.remove(thread)
|
||||
|
||||
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.TextAlignmentRole:
|
||||
return QtCore.Qt.AlignRight
|
||||
if role == QtCore.Qt.ForegroundRole:
|
||||
return self._version_font_color
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
role = PROJECT_VERSION_ROLE
|
||||
|
||||
return super(ProjectModel, self).data(index, role)
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
if index.column() == 1:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
role = PROJECT_VERSION_ROLE
|
||||
return super(ProjectModel, self).setData(index, value, role)
|
||||
|
||||
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section == 0:
|
||||
return "Project name"
|
||||
|
||||
elif section == 1:
|
||||
return "Used version"
|
||||
return ""
|
||||
return super(ProjectModel, self).headerData(
|
||||
section, orientation, role
|
||||
)
|
||||
|
||||
|
||||
class VersionAction(QtWidgets.QAction):
|
||||
version_triggered = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, version, *args, **kwargs):
|
||||
super(VersionAction, self).__init__(version, *args, **kwargs)
|
||||
self._version = version
|
||||
self.triggered.connect(self._on_trigger)
|
||||
|
||||
def _on_trigger(self):
|
||||
self.version_triggered.emit(self._version)
|
||||
|
||||
|
||||
class ProjectView(QtWidgets.QTreeView):
|
||||
left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)
|
||||
right_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectView, self).__init__(*args, **kwargs)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.setIndentation(0)
|
||||
|
||||
# Do not allow editing
|
||||
self.setEditTriggers(
|
||||
QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
|
||||
)
|
||||
# Do not automatically handle selection
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
index = self.indexAt(event.pos())
|
||||
self.left_mouse_released_at.emit(index)
|
||||
super(ProjectListView, self).mouseReleaseEvent(event)
|
||||
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
index = self.indexAt(event.pos())
|
||||
self.right_mouse_released_at.emit(index)
|
||||
|
||||
super(ProjectView, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
|
|
@ -846,18 +1000,21 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
|
||||
class ProjectListWidget(QtWidgets.QWidget):
|
||||
project_changed = QtCore.Signal()
|
||||
version_change_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent, only_active=False):
|
||||
self._parent = parent
|
||||
|
||||
self._entity = None
|
||||
self.current_project = None
|
||||
|
||||
super(ProjectListWidget, self).__init__(parent)
|
||||
self.setObjectName("ProjectListWidget")
|
||||
|
||||
label_widget = QtWidgets.QLabel("Projects")
|
||||
content_frame = QtWidgets.QFrame(self)
|
||||
content_frame.setObjectName("ProjectListContentWidget")
|
||||
|
||||
project_list = ProjectListView(self)
|
||||
project_list = ProjectView(content_frame)
|
||||
project_model = ProjectModel(only_active)
|
||||
project_proxy = ProjectSortFilterProxy()
|
||||
|
||||
|
|
@ -865,33 +1022,37 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
project_proxy.setSourceModel(project_model)
|
||||
project_list.setModel(project_proxy)
|
||||
|
||||
# Do not allow editing
|
||||
project_list.setEditTriggers(
|
||||
QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
|
||||
)
|
||||
# Do not automatically handle selection
|
||||
project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
content_layout = QtWidgets.QVBoxLayout(content_frame)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(0)
|
||||
content_layout.addWidget(project_list, 1)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setSpacing(3)
|
||||
layout.addWidget(label_widget, 0)
|
||||
layout.addWidget(project_list, 1)
|
||||
inactive_chk = None
|
||||
if not only_active:
|
||||
checkbox_wrapper = QtWidgets.QWidget(content_frame)
|
||||
checkbox_wrapper.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
if only_active:
|
||||
inactive_chk = None
|
||||
else:
|
||||
inactive_chk = QtWidgets.QCheckBox(" Show Inactive Projects ")
|
||||
inactive_chk = QtWidgets.QCheckBox(
|
||||
"Show Inactive Projects", checkbox_wrapper
|
||||
)
|
||||
inactive_chk.setChecked(not project_proxy.is_filter_enabled())
|
||||
|
||||
layout.addSpacing(5)
|
||||
layout.addWidget(inactive_chk, 0)
|
||||
layout.addSpacing(5)
|
||||
wrapper_layout = QtWidgets.QHBoxLayout(checkbox_wrapper)
|
||||
wrapper_layout.addWidget(inactive_chk, 1)
|
||||
|
||||
content_layout.addWidget(checkbox_wrapper, 0)
|
||||
|
||||
inactive_chk.stateChanged.connect(self.on_inactive_vis_changed)
|
||||
|
||||
project_list.left_mouse_released_at.connect(self.on_item_clicked)
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
# Margins '3' are matching to configurables widget scroll area on right
|
||||
layout.setContentsMargins(5, 3, 3, 3)
|
||||
layout.addWidget(content_frame, 1)
|
||||
|
||||
self._default_project_item = None
|
||||
project_list.left_mouse_released_at.connect(self.on_item_clicked)
|
||||
project_list.right_mouse_released_at.connect(
|
||||
self._on_item_right_clicked
|
||||
)
|
||||
|
||||
self.project_list = project_list
|
||||
self.project_proxy = project_proxy
|
||||
|
|
@ -900,10 +1061,46 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
|
||||
self.dbcon = None
|
||||
|
||||
def on_item_clicked(self, new_index):
|
||||
new_project_name = new_index.data(QtCore.Qt.DisplayRole)
|
||||
if new_project_name is None:
|
||||
def set_entity(self, entity):
|
||||
self._entity = entity
|
||||
|
||||
def _on_item_right_clicked(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if project_name is None:
|
||||
project_name = DEFAULT_PROJECT_LABEL
|
||||
|
||||
if self.current_project != project_name:
|
||||
self.on_item_clicked(index)
|
||||
|
||||
if self.current_project != project_name:
|
||||
return
|
||||
|
||||
if not self._entity:
|
||||
return
|
||||
|
||||
versions = self._entity.get_available_source_versions(sorted=True)
|
||||
if not versions:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
submenu = QtWidgets.QMenu("Use settings from version", menu)
|
||||
for version in reversed(versions):
|
||||
action = VersionAction(version, submenu)
|
||||
action.version_triggered.connect(
|
||||
self.version_change_requested
|
||||
)
|
||||
submenu.addAction(action)
|
||||
menu.addMenu(submenu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def on_item_clicked(self, new_index):
|
||||
if not new_index.isValid():
|
||||
return
|
||||
new_project_name = new_index.data(PROJECT_NAME_ROLE)
|
||||
if new_project_name is None:
|
||||
new_project_name = DEFAULT_PROJECT_LABEL
|
||||
|
||||
if self.current_project == new_project_name:
|
||||
return
|
||||
|
|
@ -963,12 +1160,30 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
index = model.indexFromItem(found_items[0])
|
||||
model.setData(index, True, PROJECT_IS_SELECTED_ROLE)
|
||||
|
||||
index = proxy.mapFromSource(index)
|
||||
src_indexes = []
|
||||
col_count = model.columnCount()
|
||||
if col_count > 1:
|
||||
for col in range(col_count):
|
||||
src_indexes.append(
|
||||
model.index(index.row(), col, index.parent())
|
||||
)
|
||||
dst_indexes = []
|
||||
for index in src_indexes:
|
||||
dst_indexes.append(proxy.mapFromSource(index))
|
||||
|
||||
self.project_list.selectionModel().clear()
|
||||
self.project_list.selectionModel().setCurrentIndex(
|
||||
index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent
|
||||
)
|
||||
selection_model = self.project_list.selectionModel()
|
||||
selection_model.clear()
|
||||
|
||||
first = True
|
||||
for index in dst_indexes:
|
||||
if first:
|
||||
selection_model.setCurrentIndex(
|
||||
index,
|
||||
QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent
|
||||
)
|
||||
first = False
|
||||
continue
|
||||
selection_model.select(index, QtCore.QItemSelectionModel.Select)
|
||||
|
||||
def get_project_names(self):
|
||||
output = []
|
||||
|
|
@ -980,7 +1195,7 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
def refresh(self):
|
||||
selected_project = None
|
||||
for index in self.project_list.selectedIndexes():
|
||||
selected_project = index.data(QtCore.Qt.DisplayRole)
|
||||
selected_project = index.data(PROJECT_NAME_ROLE)
|
||||
break
|
||||
|
||||
mongo_url = os.environ["OPENPYPE_MONGO"]
|
||||
|
|
@ -1008,5 +1223,6 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
self.select_project(selected_project)
|
||||
|
||||
self.current_project = self.project_list.currentIndex().data(
|
||||
QtCore.Qt.DisplayRole
|
||||
PROJECT_NAME_ROLE
|
||||
)
|
||||
self.project_list.resizeColumnToContents(0)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ from .categories import (
|
|||
SystemWidget,
|
||||
ProjectWidget
|
||||
)
|
||||
from .widgets import ShadowWidget, RestartDialog
|
||||
from .widgets import (
|
||||
ShadowWidget,
|
||||
RestartDialog,
|
||||
SettingsTabWidget
|
||||
)
|
||||
from .search_dialog import SearchEntitiesDialog
|
||||
from openpype import style
|
||||
|
||||
|
|
@ -35,7 +39,7 @@ class MainWidget(QtWidgets.QWidget):
|
|||
self.setStyleSheet(stylesheet)
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
|
||||
header_tab_widget = QtWidgets.QTabWidget(parent=self)
|
||||
header_tab_widget = SettingsTabWidget(parent=self)
|
||||
|
||||
studio_widget = SystemWidget(user_role, header_tab_widget)
|
||||
project_widget = ProjectWidget(user_role, header_tab_widget)
|
||||
|
|
@ -73,6 +77,10 @@ class MainWidget(QtWidgets.QWidget):
|
|||
tab_widget.reset_started.connect(self._on_reset_finished)
|
||||
tab_widget.full_path_requested.connect(self._on_full_path_request)
|
||||
|
||||
header_tab_widget.context_menu_requested.connect(
|
||||
self._on_context_menu_request
|
||||
)
|
||||
|
||||
self._header_tab_widget = header_tab_widget
|
||||
self.tab_widgets = tab_widgets
|
||||
self._search_dialog = search_dialog
|
||||
|
|
@ -109,6 +117,18 @@ class MainWidget(QtWidgets.QWidget):
|
|||
tab_widget.set_category_path(category, path)
|
||||
break
|
||||
|
||||
def _on_context_menu_request(self, tab_idx):
|
||||
widget = self._header_tab_widget.widget(tab_idx)
|
||||
if not widget:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
widget.add_context_actions(menu)
|
||||
if menu.actions():
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result is not None:
|
||||
self._header_tab_widget.setCurrentIndex(tab_idx)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(MainWidget, self).showEvent(event)
|
||||
if self._reset_on_show:
|
||||
|
|
|
|||
173
openpype/tools/stdout_broker/app.py
Normal file
173
openpype/tools/stdout_broker/app.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import os
|
||||
import sys
|
||||
import threading
|
||||
import collections
|
||||
import websocket
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from openpype_modules.webserver.host_console_listener import MsgAction
|
||||
from openpype.api import Logger
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class StdOutBroker:
|
||||
"""
|
||||
Application showing console in Services tray for non python hosts
|
||||
instead of cmd window.
|
||||
"""
|
||||
MAX_LINES = 10000
|
||||
TIMER_TIMEOUT = 0.200
|
||||
|
||||
def __init__(self, host_name):
|
||||
self.host_name = host_name
|
||||
self.webserver_client = None
|
||||
|
||||
self.original_stdout_write = None
|
||||
self.original_stderr_write = None
|
||||
self.log_queue = collections.deque()
|
||||
|
||||
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
|
||||
self.host_id = "{}_{}".format(self.host_name, date_str)
|
||||
|
||||
self._std_available = False
|
||||
self._is_running = False
|
||||
self._catch_std_outputs()
|
||||
|
||||
self._timer = None
|
||||
|
||||
@property
|
||||
def send_to_tray(self):
|
||||
"""Checks if connected to tray and have access to logs."""
|
||||
return self.webserver_client and self._std_available
|
||||
|
||||
def start(self):
|
||||
"""Start app, create and start timer"""
|
||||
if not self._std_available or self._is_running:
|
||||
return
|
||||
self._is_running = True
|
||||
self._create_timer()
|
||||
self._connect_to_tray()
|
||||
|
||||
def stop(self):
|
||||
"""Disconnect from Tray, process last logs"""
|
||||
if not self._is_running:
|
||||
return
|
||||
self._is_running = False
|
||||
self._process_queue()
|
||||
self._disconnect_from_tray()
|
||||
|
||||
def host_connected(self):
|
||||
"""Send to Tray console that host is ready - icon change. """
|
||||
log.info("Host {} connected".format(self.host_id))
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": MsgAction.INITIALIZED,
|
||||
"text": "Integration with {}".format(
|
||||
str.capitalize(self.host_name))
|
||||
}
|
||||
self._send(payload)
|
||||
|
||||
def _create_timer(self):
|
||||
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
|
||||
timer.start()
|
||||
self._timer = timer
|
||||
|
||||
def _timer_callback(self):
|
||||
if not self._is_running:
|
||||
return
|
||||
self._process_queue()
|
||||
self._create_timer()
|
||||
|
||||
def _connect_to_tray(self):
|
||||
"""Connect to Tray webserver to pass console output. """
|
||||
if not self._std_available: # not content to log
|
||||
return
|
||||
ws = websocket.WebSocket()
|
||||
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
|
||||
|
||||
if not webserver_url:
|
||||
print("Unknown webserver url, cannot connect to pass log")
|
||||
return
|
||||
|
||||
webserver_url = webserver_url.replace("http", "ws")
|
||||
ws.connect("{}/ws/host_listener".format(webserver_url))
|
||||
self.webserver_client = ws
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": MsgAction.CONNECTING,
|
||||
"text": "Integration with {}".format(
|
||||
str.capitalize(self.host_name))
|
||||
}
|
||||
self._send(payload)
|
||||
|
||||
def _disconnect_from_tray(self):
|
||||
"""Send to Tray that host is closing - remove from Services. """
|
||||
print("Host {} closing".format(self.host_name))
|
||||
if not self.webserver_client:
|
||||
return
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": MsgAction.CLOSE,
|
||||
"text": "Integration with {}".format(
|
||||
str.capitalize(self.host_name))
|
||||
}
|
||||
|
||||
self._send(payload)
|
||||
self.webserver_client.close()
|
||||
|
||||
def _catch_std_outputs(self):
|
||||
"""Redirects standard out and error to own functions"""
|
||||
if sys.stdout:
|
||||
self.original_stdout_write = sys.stdout.write
|
||||
sys.stdout.write = self._my_stdout_write
|
||||
self._std_available = True
|
||||
|
||||
if sys.stderr:
|
||||
self.original_stderr_write = sys.stderr.write
|
||||
sys.stderr.write = self._my_stderr_write
|
||||
self._std_available = True
|
||||
|
||||
def _my_stdout_write(self, text):
|
||||
"""Appends outputted text to queue, keep writing to original stdout"""
|
||||
if self.original_stdout_write is not None:
|
||||
self.original_stdout_write(text)
|
||||
if self.send_to_tray:
|
||||
self.log_queue.append(text)
|
||||
|
||||
def _my_stderr_write(self, text):
|
||||
"""Appends outputted text to queue, keep writing to original stderr"""
|
||||
if self.original_stderr_write is not None:
|
||||
self.original_stderr_write(text)
|
||||
if self.send_to_tray:
|
||||
self.log_queue.append(text)
|
||||
|
||||
def _process_queue(self):
|
||||
"""Sends lines and purges queue"""
|
||||
if not self.send_to_tray:
|
||||
return
|
||||
|
||||
lines = tuple(self.log_queue)
|
||||
self.log_queue.clear()
|
||||
if lines:
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": MsgAction.ADD,
|
||||
"text": "\n".join(lines)
|
||||
}
|
||||
|
||||
self._send(payload)
|
||||
|
||||
def _send(self, payload):
|
||||
"""Worker method to send to existing websocket connection."""
|
||||
if not self.send_to_tray:
|
||||
return
|
||||
|
||||
try:
|
||||
self.webserver_client.send(json.dumps(payload))
|
||||
except ConnectionResetError: # Tray closed
|
||||
self._connect_to_tray()
|
||||
103
openpype/tools/stdout_broker/window.py
Normal file
103
openpype/tools/stdout_broker/window.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
from avalon import style
|
||||
from Qt import QtWidgets, QtCore
|
||||
import collections
|
||||
import re
|
||||
|
||||
|
||||
class ConsoleDialog(QtWidgets.QDialog):
|
||||
"""Qt dialog to show stdout instead of unwieldy cmd window"""
|
||||
WIDTH = 720
|
||||
HEIGHT = 450
|
||||
MAX_LINES = 10000
|
||||
|
||||
sdict = {
|
||||
r">>> ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> >>> </span>',
|
||||
r"!!!(?!\sCRI|\sERR)":
|
||||
'<span style="font-weight: bold;color:red"> !!! </span>',
|
||||
r"\-\-\- ":
|
||||
'<span style="font-weight: bold;color:cyan"> --- </span>',
|
||||
r"\*\*\*(?!\sWRN)":
|
||||
'<span style="font-weight: bold;color:#FFD700"> *** </span>',
|
||||
r"\*\*\* WRN":
|
||||
'<span style="font-weight: bold;color:#FFD700"> *** WRN</span>',
|
||||
r" \- ":
|
||||
'<span style="font-weight: bold;color:#FFD700"> - </span>',
|
||||
r"\[ ":
|
||||
'<span style="font-weight: bold;color:#66CDAA">[</span>',
|
||||
r"\]":
|
||||
'<span style="font-weight: bold;color:#66CDAA">]</span>',
|
||||
r"{":
|
||||
'<span style="color:#66CDAA">{',
|
||||
r"}":
|
||||
r"}</span>",
|
||||
r"\(":
|
||||
'<span style="color:#66CDAA">(',
|
||||
r"\)":
|
||||
r")</span>",
|
||||
r"^\.\.\. ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> ... </span>',
|
||||
r"!!! ERR: ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> !!! ERR: </span>',
|
||||
r"!!! CRI: ":
|
||||
'<span style="font-weight: bold;color:red"> !!! CRI: </span>',
|
||||
r"(?i)failed":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> FAILED </span>',
|
||||
r"(?i)error":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> ERROR </span>'
|
||||
}
|
||||
|
||||
def __init__(self, text, parent=None):
|
||||
super(ConsoleDialog, self).__init__(parent)
|
||||
layout = QtWidgets.QHBoxLayout(parent)
|
||||
|
||||
plain_text = QtWidgets.QPlainTextEdit(self)
|
||||
plain_text.setReadOnly(True)
|
||||
plain_text.resize(self.WIDTH, self.HEIGHT)
|
||||
plain_text.maximumBlockCount = self.MAX_LINES
|
||||
|
||||
while text:
|
||||
plain_text.appendPlainText(text.popleft().strip())
|
||||
|
||||
layout.addWidget(plain_text)
|
||||
|
||||
self.setWindowTitle("Console output")
|
||||
|
||||
self.plain_text = plain_text
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.resize(self.WIDTH, self.HEIGHT)
|
||||
|
||||
def append_text(self, new_text):
|
||||
if isinstance(new_text, str):
|
||||
new_text = collections.deque(new_text.split("\n"))
|
||||
while new_text:
|
||||
text = new_text.popleft()
|
||||
if text:
|
||||
self.plain_text.appendHtml(self.color(text))
|
||||
|
||||
def _multiple_replace(self, text, adict):
|
||||
"""Replace multiple tokens defined in dict.
|
||||
|
||||
Find and replace all occurrences of strings defined in dict is
|
||||
supplied string.
|
||||
|
||||
Args:
|
||||
text (str): string to be searched
|
||||
adict (dict): dictionary with `{'search': 'replace'}`
|
||||
|
||||
Returns:
|
||||
str: string with replaced tokens
|
||||
|
||||
"""
|
||||
for r, v in adict.items():
|
||||
text = re.sub(r, v, text)
|
||||
|
||||
return text
|
||||
|
||||
def color(self, message):
|
||||
"""Color message with html tags. """
|
||||
message = self._multiple_replace(message, self.sdict)
|
||||
|
||||
return message
|
||||
|
|
@ -33,7 +33,8 @@ from openpype.settings import (
|
|||
)
|
||||
from openpype.tools.utils import (
|
||||
WrappedCallbackItem,
|
||||
paint_image_with_color
|
||||
paint_image_with_color,
|
||||
get_warning_pixmap
|
||||
)
|
||||
|
||||
from .pype_info_widget import PypeInfoWidget
|
||||
|
|
@ -76,7 +77,7 @@ class PixmapLabel(QtWidgets.QLabel):
|
|||
super(PixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class VersionDialog(QtWidgets.QDialog):
|
||||
class VersionUpdateDialog(QtWidgets.QDialog):
|
||||
restart_requested = QtCore.Signal()
|
||||
ignore_requested = QtCore.Signal()
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ class VersionDialog(QtWidgets.QDialog):
|
|||
_min_height = 130
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(VersionDialog, self).__init__(parent)
|
||||
super(VersionUpdateDialog, self).__init__(parent)
|
||||
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
|
@ -152,11 +153,11 @@ class VersionDialog(QtWidgets.QDialog):
|
|||
)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
super(VersionUpdateDialog, self).showEvent(event)
|
||||
self._restart_accepted = False
|
||||
|
||||
def closeEvent(self, event):
|
||||
super().closeEvent(event)
|
||||
super(VersionUpdateDialog, self).closeEvent(event)
|
||||
if self._restart_accepted or self._current_is_higher:
|
||||
return
|
||||
# Trigger ignore requested only if restart was not clicked and current
|
||||
|
|
@ -202,6 +203,63 @@ class VersionDialog(QtWidgets.QDialog):
|
|||
self.accept()
|
||||
|
||||
|
||||
class BuildVersionDialog(QtWidgets.QDialog):
|
||||
"""Build/Installation version is too low for current OpenPype version.
|
||||
|
||||
This dialog tells to user that it's build OpenPype is too old.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super(BuildVersionDialog, self).__init__(parent)
|
||||
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Outdated OpenPype installation")
|
||||
self.setWindowFlags(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
top_widget = QtWidgets.QWidget(self)
|
||||
|
||||
warning_pixmap = get_warning_pixmap()
|
||||
warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
|
||||
|
||||
message = (
|
||||
"Your installation of OpenPype <b>does not match minimum"
|
||||
" requirements</b>.<br/><br/>Please update OpenPype installation"
|
||||
" to newer version."
|
||||
)
|
||||
content_label = QtWidgets.QLabel(message, self)
|
||||
|
||||
top_layout = QtWidgets.QHBoxLayout(top_widget)
|
||||
top_layout.setContentsMargins(0, 0, 0, 0)
|
||||
top_layout.addWidget(
|
||||
warning_icon_label, 0,
|
||||
QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
|
||||
)
|
||||
top_layout.addWidget(content_label, 1)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
ok_btn = QtWidgets.QPushButton("I understand", footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(ok_btn)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(top_widget, 0)
|
||||
main_layout.addStretch(1)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self.close()
|
||||
|
||||
|
||||
class TrayManager:
|
||||
"""Cares about context of application.
|
||||
|
||||
|
|
@ -272,7 +330,7 @@ class TrayManager:
|
|||
return
|
||||
|
||||
if self._version_dialog is None:
|
||||
self._version_dialog = VersionDialog()
|
||||
self._version_dialog = VersionUpdateDialog()
|
||||
self._version_dialog.restart_requested.connect(
|
||||
self._restart_and_install
|
||||
)
|
||||
|
|
@ -383,6 +441,10 @@ class TrayManager:
|
|||
|
||||
self._validate_settings_defaults()
|
||||
|
||||
if not op_version_control_available():
|
||||
dialog = BuildVersionDialog()
|
||||
dialog.exec_()
|
||||
|
||||
def _validate_settings_defaults(self):
|
||||
valid = True
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,331 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import re
|
||||
import collections
|
||||
import queue
|
||||
import websocket
|
||||
import json
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
|
||||
from avalon import style
|
||||
from openpype_modules.webserver import host_console_listener
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
class ConsoleTrayApp:
|
||||
"""
|
||||
Application showing console in Services tray for non python hosts
|
||||
instead of cmd window.
|
||||
"""
|
||||
callback_queue = None
|
||||
process = None
|
||||
webserver_client = None
|
||||
|
||||
MAX_LINES = 10000
|
||||
|
||||
sdict = {
|
||||
r">>> ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> >>> </span>',
|
||||
r"!!!(?!\sCRI|\sERR)":
|
||||
'<span style="font-weight: bold;color:red"> !!! </span>',
|
||||
r"\-\-\- ":
|
||||
'<span style="font-weight: bold;color:cyan"> --- </span>',
|
||||
r"\*\*\*(?!\sWRN)":
|
||||
'<span style="font-weight: bold;color:#FFD700"> *** </span>',
|
||||
r"\*\*\* WRN":
|
||||
'<span style="font-weight: bold;color:#FFD700"> *** WRN</span>',
|
||||
r" \- ":
|
||||
'<span style="font-weight: bold;color:#FFD700"> - </span>',
|
||||
r"\[ ":
|
||||
'<span style="font-weight: bold;color:#66CDAA">[</span>',
|
||||
r"\]":
|
||||
'<span style="font-weight: bold;color:#66CDAA">]</span>',
|
||||
r"{":
|
||||
'<span style="color:#66CDAA">{',
|
||||
r"}":
|
||||
r"}</span>",
|
||||
r"\(":
|
||||
'<span style="color:#66CDAA">(',
|
||||
r"\)":
|
||||
r")</span>",
|
||||
r"^\.\.\. ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> ... </span>',
|
||||
r"!!! ERR: ":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> !!! ERR: </span>',
|
||||
r"!!! CRI: ":
|
||||
'<span style="font-weight: bold;color:red"> !!! CRI: </span>',
|
||||
r"(?i)failed":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> FAILED </span>',
|
||||
r"(?i)error":
|
||||
'<span style="font-weight: bold;color:#EE5C42"> ERROR </span>'
|
||||
}
|
||||
|
||||
def __init__(self, host, launch_method, subprocess_args, is_host_connected,
|
||||
parent=None):
|
||||
self.host = host
|
||||
|
||||
self.initialized = False
|
||||
self.websocket_server = None
|
||||
self.initializing = False
|
||||
self.tray = False
|
||||
self.launch_method = launch_method
|
||||
self.subprocess_args = subprocess_args
|
||||
self.is_host_connected = is_host_connected
|
||||
self.tray_reconnect = True
|
||||
|
||||
self.original_stdout_write = None
|
||||
self.original_stderr_write = None
|
||||
self.new_text = collections.deque()
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(self.on_timer)
|
||||
timer.setInterval(200)
|
||||
timer.start()
|
||||
|
||||
self.timer = timer
|
||||
|
||||
self.catch_std_outputs()
|
||||
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
|
||||
self.host_id = "{}_{}".format(self.host, date_str)
|
||||
|
||||
def _connect(self):
|
||||
""" Connect to Tray webserver to pass console output. """
|
||||
ws = websocket.WebSocket()
|
||||
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
|
||||
|
||||
if not webserver_url:
|
||||
print("Unknown webserver url, cannot connect to pass log")
|
||||
self.tray_reconnect = False
|
||||
return
|
||||
|
||||
webserver_url = webserver_url.replace("http", "ws")
|
||||
ws.connect("{}/ws/host_listener".format(webserver_url))
|
||||
ConsoleTrayApp.webserver_client = ws
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": host_console_listener.MsgAction.CONNECTING,
|
||||
"text": "Integration with {}".format(str.capitalize(self.host))
|
||||
}
|
||||
self.tray_reconnect = False
|
||||
self._send(payload)
|
||||
|
||||
def _connected(self):
|
||||
""" Send to Tray console that host is ready - icon change. """
|
||||
print("Host {} connected".format(self.host))
|
||||
if not ConsoleTrayApp.webserver_client:
|
||||
return
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": host_console_listener.MsgAction.INITIALIZED,
|
||||
"text": "Integration with {}".format(str.capitalize(self.host))
|
||||
}
|
||||
self.tray_reconnect = False
|
||||
self._send(payload)
|
||||
|
||||
def _close(self):
|
||||
""" Send to Tray that host is closing - remove from Services. """
|
||||
print("Host {} closing".format(self.host))
|
||||
if not ConsoleTrayApp.webserver_client:
|
||||
return
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": host_console_listener.MsgAction.CLOSE,
|
||||
"text": "Integration with {}".format(str.capitalize(self.host))
|
||||
}
|
||||
|
||||
self._send(payload)
|
||||
self.tray_reconnect = False
|
||||
ConsoleTrayApp.webserver_client.close()
|
||||
|
||||
def _send_text_queue(self):
|
||||
"""Sends lines and purges queue"""
|
||||
lines = tuple(self.new_text)
|
||||
self.new_text.clear()
|
||||
|
||||
if lines:
|
||||
self._send_lines(lines)
|
||||
|
||||
def _send_lines(self, lines):
|
||||
""" Send console content. """
|
||||
if not ConsoleTrayApp.webserver_client:
|
||||
return
|
||||
|
||||
payload = {
|
||||
"host": self.host_id,
|
||||
"action": host_console_listener.MsgAction.ADD,
|
||||
"text": "\n".join(lines)
|
||||
}
|
||||
|
||||
self._send(payload)
|
||||
|
||||
def _send(self, payload):
|
||||
""" Worker method to send to existing websocket connection. """
|
||||
if not ConsoleTrayApp.webserver_client:
|
||||
return
|
||||
|
||||
try:
|
||||
ConsoleTrayApp.webserver_client.send(json.dumps(payload))
|
||||
except ConnectionResetError: # Tray closed
|
||||
ConsoleTrayApp.webserver_client = None
|
||||
self.tray_reconnect = True
|
||||
|
||||
def on_timer(self):
|
||||
"""Called periodically to initialize and run function on main thread"""
|
||||
if self.tray_reconnect:
|
||||
self._connect() # reconnect
|
||||
|
||||
self._send_text_queue()
|
||||
|
||||
if not self.initialized:
|
||||
if self.initializing:
|
||||
host_connected = self.is_host_connected()
|
||||
if host_connected is None: # keep trying
|
||||
return
|
||||
elif not host_connected:
|
||||
text = "{} process is not alive. Exiting".format(self.host)
|
||||
print(text)
|
||||
self._send_lines([text])
|
||||
ConsoleTrayApp.websocket_server.stop()
|
||||
sys.exit(1)
|
||||
elif host_connected:
|
||||
self.initialized = True
|
||||
self.initializing = False
|
||||
self._connected()
|
||||
|
||||
return
|
||||
|
||||
ConsoleTrayApp.callback_queue = queue.Queue()
|
||||
self.initializing = True
|
||||
|
||||
self.launch_method(*self.subprocess_args)
|
||||
elif ConsoleTrayApp.callback_queue and \
|
||||
not ConsoleTrayApp.callback_queue.empty():
|
||||
try:
|
||||
callback = ConsoleTrayApp.callback_queue.get(block=False)
|
||||
callback()
|
||||
except queue.Empty:
|
||||
pass
|
||||
elif ConsoleTrayApp.process.poll() is not None:
|
||||
self.exit()
|
||||
|
||||
@classmethod
|
||||
def execute_in_main_thread(cls, func_to_call_from_main_thread):
|
||||
"""Put function to the queue to be picked by 'on_timer'"""
|
||||
if not cls.callback_queue:
|
||||
cls.callback_queue = queue.Queue()
|
||||
cls.callback_queue.put(func_to_call_from_main_thread)
|
||||
|
||||
@classmethod
|
||||
def restart_server(cls):
|
||||
if ConsoleTrayApp.websocket_server:
|
||||
ConsoleTrayApp.websocket_server.stop_server(restart=True)
|
||||
|
||||
# obsolete
|
||||
def exit(self):
|
||||
""" Exit whole application. """
|
||||
self._close()
|
||||
if ConsoleTrayApp.websocket_server:
|
||||
ConsoleTrayApp.websocket_server.stop()
|
||||
if ConsoleTrayApp.process:
|
||||
ConsoleTrayApp.process.kill()
|
||||
ConsoleTrayApp.process.wait()
|
||||
if self.timer:
|
||||
self.timer.stop()
|
||||
QtCore.QCoreApplication.exit()
|
||||
|
||||
def catch_std_outputs(self):
|
||||
"""Redirects standard out and error to own functions"""
|
||||
if not sys.stdout:
|
||||
self.dialog.append_text("Cannot read from stdout!")
|
||||
else:
|
||||
self.original_stdout_write = sys.stdout.write
|
||||
sys.stdout.write = self.my_stdout_write
|
||||
|
||||
if not sys.stderr:
|
||||
self.dialog.append_text("Cannot read from stderr!")
|
||||
else:
|
||||
self.original_stderr_write = sys.stderr.write
|
||||
sys.stderr.write = self.my_stderr_write
|
||||
|
||||
def my_stdout_write(self, text):
|
||||
"""Appends outputted text to queue, keep writing to original stdout"""
|
||||
if self.original_stdout_write is not None:
|
||||
self.original_stdout_write(text)
|
||||
self.new_text.append(text)
|
||||
|
||||
def my_stderr_write(self, text):
|
||||
"""Appends outputted text to queue, keep writing to original stderr"""
|
||||
if self.original_stderr_write is not None:
|
||||
self.original_stderr_write(text)
|
||||
self.new_text.append(text)
|
||||
|
||||
@staticmethod
|
||||
def _multiple_replace(text, adict):
|
||||
"""Replace multiple tokens defined in dict.
|
||||
|
||||
Find and replace all occurrences of strings defined in dict is
|
||||
supplied string.
|
||||
|
||||
Args:
|
||||
text (str): string to be searched
|
||||
adict (dict): dictionary with `{'search': 'replace'}`
|
||||
|
||||
Returns:
|
||||
str: string with replaced tokens
|
||||
|
||||
"""
|
||||
for r, v in adict.items():
|
||||
text = re.sub(r, v, text)
|
||||
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def color(message):
|
||||
""" Color message with html tags. """
|
||||
message = ConsoleTrayApp._multiple_replace(message,
|
||||
ConsoleTrayApp.sdict)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class ConsoleDialog(QtWidgets.QDialog):
|
||||
"""Qt dialog to show stdout instead of unwieldy cmd window"""
|
||||
WIDTH = 720
|
||||
HEIGHT = 450
|
||||
MAX_LINES = 10000
|
||||
|
||||
def __init__(self, text, parent=None):
|
||||
super(ConsoleDialog, self).__init__(parent)
|
||||
layout = QtWidgets.QHBoxLayout(parent)
|
||||
|
||||
plain_text = QtWidgets.QPlainTextEdit(self)
|
||||
plain_text.setReadOnly(True)
|
||||
plain_text.resize(self.WIDTH, self.HEIGHT)
|
||||
plain_text.maximumBlockCount = self.MAX_LINES
|
||||
|
||||
while text:
|
||||
plain_text.appendPlainText(text.popleft().strip())
|
||||
|
||||
layout.addWidget(plain_text)
|
||||
|
||||
self.setWindowTitle("Console output")
|
||||
|
||||
self.plain_text = plain_text
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.resize(self.WIDTH, self.HEIGHT)
|
||||
|
||||
def append_text(self, new_text):
|
||||
if isinstance(new_text, str):
|
||||
new_text = collections.deque(new_text.split("\n"))
|
||||
while new_text:
|
||||
text = new_text.popleft()
|
||||
if text:
|
||||
self.plain_text.appendHtml(
|
||||
ConsoleTrayApp.color(text))
|
||||
|
|
@ -3,23 +3,38 @@ from .widgets import (
|
|||
BaseClickableFrame,
|
||||
ClickableFrame,
|
||||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
)
|
||||
|
||||
from .error_dialog import ErrorMessageBox
|
||||
from .lib import (
|
||||
WrappedCallbackItem,
|
||||
paint_image_with_color
|
||||
paint_image_with_color,
|
||||
get_warning_pixmap,
|
||||
set_style_property,
|
||||
DynamicQThread,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"PlaceholderLineEdit",
|
||||
"BaseClickableFrame",
|
||||
"ClickableFrame",
|
||||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
"WrappedCallbackItem",
|
||||
"paint_image_with_color",
|
||||
"get_warning_pixmap",
|
||||
"set_style_property",
|
||||
"DynamicQThread",
|
||||
|
||||
"RecursiveSortFilterProxyModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ class AssetModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._doc_fetched.connect(self._on_docs_fetched)
|
||||
|
||||
self._items_with_color_by_id = {}
|
||||
self._item_ids_with_color = set()
|
||||
self._items_by_asset_id = {}
|
||||
|
||||
self._last_project_name = None
|
||||
|
|
@ -382,9 +382,11 @@ class AssetModel(QtGui.QStandardItemModel):
|
|||
self._stop_fetch_thread()
|
||||
|
||||
def clear_underlines(self):
|
||||
for asset_id in tuple(self._items_with_color_by_id.keys()):
|
||||
item = self._items_with_color_by_id.pop(asset_id)
|
||||
item.setData(None, ASSET_UNDERLINE_COLORS_ROLE)
|
||||
for asset_id in set(self._item_ids_with_color):
|
||||
self._item_ids_with_color.remove(asset_id)
|
||||
item = self._items_by_asset_id.get(asset_id)
|
||||
if item is not None:
|
||||
item.setData(None, ASSET_UNDERLINE_COLORS_ROLE)
|
||||
|
||||
def set_underline_colors(self, colors_by_asset_id):
|
||||
self.clear_underlines()
|
||||
|
|
@ -394,12 +396,13 @@ class AssetModel(QtGui.QStandardItemModel):
|
|||
if item is None:
|
||||
continue
|
||||
item.setData(colors, ASSET_UNDERLINE_COLORS_ROLE)
|
||||
self._item_ids_with_color.add(asset_id)
|
||||
|
||||
def _clear_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._items_by_asset_id = {}
|
||||
self._items_with_color_by_id = {}
|
||||
self._item_ids_with_color = set()
|
||||
|
||||
def _on_docs_fetched(self):
|
||||
# Make sure refreshing did not change
|
||||
|
|
@ -635,9 +638,10 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
refresh_btn.clicked.connect(self.refresh)
|
||||
current_asset_btn.clicked.connect(self.set_current_session_asset)
|
||||
current_asset_btn.clicked.connect(self._on_current_asset_click)
|
||||
view.doubleClicked.connect(self.double_clicked)
|
||||
|
||||
self._refresh_btn = refresh_btn
|
||||
self._current_asset_btn = current_asset_btn
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
|
|
@ -668,11 +672,30 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
def stop_refresh(self):
|
||||
self._model.stop_refresh()
|
||||
|
||||
def _get_current_session_asset(self):
|
||||
return self.dbcon.Session.get("AVALON_ASSET")
|
||||
|
||||
def _on_current_asset_click(self):
|
||||
"""Trigger change of asset to current context asset.
|
||||
This separation gives ability to override this method and use it
|
||||
in differnt way.
|
||||
"""
|
||||
self.set_current_session_asset()
|
||||
|
||||
def set_current_session_asset(self):
|
||||
asset_name = self.dbcon.Session.get("AVALON_ASSET")
|
||||
asset_name = self._get_current_session_asset()
|
||||
if asset_name:
|
||||
self.select_asset_by_name(asset_name)
|
||||
|
||||
def set_refresh_btn_visibility(self, visible=None):
|
||||
"""Hide set refresh button.
|
||||
Some tools may have their global refresh button or do not support
|
||||
refresh at all.
|
||||
"""
|
||||
if visible is None:
|
||||
visible = not self._refresh_btn.isVisible()
|
||||
self._refresh_btn.setVisible(visible)
|
||||
|
||||
def set_current_asset_btn_visibility(self, visible=None):
|
||||
"""Hide set current asset button.
|
||||
|
||||
|
|
@ -727,6 +750,10 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
def _set_loading_state(self, loading, empty):
|
||||
self._view.set_loading_state(loading, empty)
|
||||
|
||||
def _clear_selection(self):
|
||||
selection_model = self._view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
def _select_indexes(self, indexes):
|
||||
valid_indexes = [
|
||||
index
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Qt
|
|||
from Qt import QtWidgets, QtGui, QtCore
|
||||
|
||||
from avalon.lib import HeroVersionType
|
||||
from openpype.style import get_objected_colors
|
||||
from .models import TreeModel
|
||||
from . import lib
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from openpype.api import (
|
|||
Logger
|
||||
)
|
||||
from openpype.lib import filter_profiles
|
||||
from openpype.style import get_objected_colors
|
||||
from openpype.resources import get_image_path
|
||||
|
||||
|
||||
def center_window(window):
|
||||
|
|
@ -28,6 +30,18 @@ def center_window(window):
|
|||
window.move(geo.topLeft())
|
||||
|
||||
|
||||
def set_style_property(widget, property_name, property_value):
|
||||
"""Set widget's property that may affect style.
|
||||
|
||||
If current property value is different then style of widget is polished.
|
||||
"""
|
||||
cur_value = widget.property(property_name)
|
||||
if cur_value == property_value:
|
||||
return
|
||||
widget.setProperty(property_name, property_value)
|
||||
widget.style().polish(widget)
|
||||
|
||||
|
||||
def paint_image_with_color(image, color):
|
||||
"""Redraw image with single color using it's alpha.
|
||||
|
||||
|
|
@ -670,3 +684,19 @@ class WrappedCallbackItem:
|
|||
|
||||
finally:
|
||||
self._done = True
|
||||
|
||||
|
||||
def get_warning_pixmap(color=None):
|
||||
"""Warning icon as QPixmap.
|
||||
|
||||
Args:
|
||||
color(QtGui.QColor): Color that will be used to paint warning icon.
|
||||
"""
|
||||
src_image_path = get_image_path("warning.png")
|
||||
src_image = QtGui.QImage(src_image_path)
|
||||
if color is None:
|
||||
colors = get_objected_colors()
|
||||
color_value = colors["delete-btn-bg"]
|
||||
color = color_value.get_qcolor()
|
||||
|
||||
return paint_image_with_color(src_image, color)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import logging
|
|||
|
||||
import Qt
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import style, io
|
||||
from . import lib
|
||||
from .constants import (
|
||||
PROJECT_IS_ACTIVE_ROLE,
|
||||
PROJECT_NAME_ROLE,
|
||||
|
|
@ -199,31 +196,37 @@ class Item(dict):
|
|||
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
"""Recursive proxy model.
|
||||
Item is not filtered if any children match the filter.
|
||||
Use case: Filtering by string - parent won't be filtered if does not match
|
||||
the filter string but first checks if any children does.
|
||||
"""
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
pattern = regex.pattern()
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
source_index = model.index(
|
||||
row, self.filterKeyColumn(), parent_index
|
||||
)
|
||||
if source_index.isValid():
|
||||
pattern = regex.pattern()
|
||||
|
||||
# Check current index itself
|
||||
key = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
value = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, value, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for i in range(rows):
|
||||
if self.filterAcceptsRow(i, source_index):
|
||||
for idx in range(rows):
|
||||
if self.filterAcceptsRow(idx, source_index):
|
||||
return True
|
||||
|
||||
# Otherwise filter it
|
||||
return False
|
||||
|
||||
return super(
|
||||
RecursiveSortFilterProxyModel, self
|
||||
).filterAcceptsRow(row, parent)
|
||||
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
|
||||
row, parent_index
|
||||
)
|
||||
|
||||
|
||||
class ProjectModel(QtGui.QStandardItemModel):
|
||||
|
|
|
|||
|
|
@ -255,6 +255,10 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def _clear_selection(self):
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
def select_task_name(self, task_name):
|
||||
"""Select a task by name.
|
||||
|
||||
|
|
@ -285,6 +289,10 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
self._tasks_view.setCurrentIndex(index)
|
||||
break
|
||||
|
||||
last_selected_task_name = self.get_selected_task_name()
|
||||
if last_selected_task_name:
|
||||
self._last_selected_task_name = last_selected_task_name
|
||||
|
||||
def get_selected_task_name(self):
|
||||
"""Return name of task at current index (selected)
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,65 @@ class ImageButton(QtWidgets.QPushButton):
|
|||
return self.iconSize()
|
||||
|
||||
|
||||
class IconButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
Using font metrics height as icon size reference.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IconButton, self).__init__(*args, **kwargs)
|
||||
self.setObjectName("IconButton")
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(IconButton, self).sizeHint()
|
||||
icon_h = self.iconSize().height()
|
||||
font_height = self.fontMetrics().height()
|
||||
text_set = bool(self.text())
|
||||
if not text_set and icon_h < font_height:
|
||||
new_size = result.height() - icon_h + font_height
|
||||
result.setHeight(new_size)
|
||||
result.setWidth(new_size)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PixmapLabel(QtWidgets.QLabel):
|
||||
"""Label resizing image to height of font."""
|
||||
def __init__(self, pixmap, parent):
|
||||
super(PixmapLabel, self).__init__(parent)
|
||||
self._empty_pixmap = QtGui.QPixmap(0, 0)
|
||||
self._source_pixmap = pixmap
|
||||
|
||||
def set_source_pixmap(self, pixmap):
|
||||
"""Change source image."""
|
||||
self._source_pixmap = pixmap
|
||||
self._set_resized_pix()
|
||||
|
||||
def _get_pix_size(self):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
return size, size
|
||||
|
||||
def _set_resized_pix(self):
|
||||
if self._source_pixmap is None:
|
||||
self.setPixmap(self._empty_pixmap)
|
||||
return
|
||||
width, height = self._get_pix_size()
|
||||
self.setPixmap(
|
||||
self._source_pixmap.scaled(
|
||||
width,
|
||||
height,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
super(PixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class OptionalMenu(QtWidgets.QMenu):
|
||||
"""A subclass of `QtWidgets.QMenu` to work with `OptionalAction`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import sys
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import getpass
|
||||
import shutil
|
||||
|
|
@ -21,14 +22,13 @@ from openpype.tools.utils.tasks_widget import TasksWidget
|
|||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
from openpype.lib import (
|
||||
Anatomy,
|
||||
get_workdir,
|
||||
get_workfile_doc,
|
||||
create_workfile_doc,
|
||||
save_workfile_data_to_doc,
|
||||
get_workfile_template_key,
|
||||
create_workdir_extra_folders
|
||||
create_workdir_extra_folders,
|
||||
get_system_general_anatomy_data
|
||||
)
|
||||
|
||||
from .model import FilesModel
|
||||
from .view import FilesView
|
||||
|
||||
|
|
@ -38,6 +38,185 @@ module = sys.modules[__name__]
|
|||
module.window = None
|
||||
|
||||
|
||||
def build_workfile_data(session):
|
||||
"""Get the data required for workfile formatting from avalon `session`"""
|
||||
|
||||
# Set work file data for template formatting
|
||||
asset_name = session["AVALON_ASSET"]
|
||||
task_name = session["AVALON_TASK"]
|
||||
project_doc = io.find_one(
|
||||
{"type": "project"},
|
||||
{
|
||||
"name": True,
|
||||
"data.code": True,
|
||||
"config.tasks": True,
|
||||
}
|
||||
)
|
||||
|
||||
asset_doc = io.find_one(
|
||||
{
|
||||
"type": "asset",
|
||||
"name": asset_name
|
||||
},
|
||||
{
|
||||
"data.tasks": True,
|
||||
"data.parents": True
|
||||
}
|
||||
)
|
||||
|
||||
task_type = asset_doc["data"]["tasks"].get(task_name, {}).get("type")
|
||||
|
||||
project_task_types = project_doc["config"]["tasks"]
|
||||
task_short = project_task_types.get(task_type, {}).get("short_name")
|
||||
|
||||
asset_parents = asset_doc["data"]["parents"]
|
||||
parent_name = project_doc["name"]
|
||||
if asset_parents:
|
||||
parent_name = asset_parents[-1]
|
||||
|
||||
data = {
|
||||
"project": {
|
||||
"name": project_doc["name"],
|
||||
"code": project_doc["data"].get("code")
|
||||
},
|
||||
"asset": asset_name,
|
||||
"task": {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_short,
|
||||
},
|
||||
"parent": parent_name,
|
||||
"version": 1,
|
||||
"user": getpass.getuser(),
|
||||
"comment": "",
|
||||
"ext": None
|
||||
}
|
||||
|
||||
# add system general settings anatomy data
|
||||
system_general_data = get_system_general_anatomy_data()
|
||||
data.update(system_general_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CommentMatcher(object):
|
||||
"""Use anatomy and work file data to parse comments from filenames"""
|
||||
def __init__(self, anatomy, template_key, data):
|
||||
|
||||
self.fname_regex = None
|
||||
|
||||
template = anatomy.templates[template_key]["file"]
|
||||
if "{comment}" not in template:
|
||||
# Don't look for comment if template doesn't allow it
|
||||
return
|
||||
|
||||
# Create a regex group for extensions
|
||||
extensions = api.registered_host().file_extensions()
|
||||
any_extension = "(?:{})".format(
|
||||
"|".join(re.escape(ext[1:]) for ext in extensions)
|
||||
)
|
||||
|
||||
# Use placeholders that will never be in the filename
|
||||
temp_data = copy.deepcopy(data)
|
||||
temp_data["comment"] = "<<comment>>"
|
||||
temp_data["version"] = "<<version>>"
|
||||
temp_data["ext"] = "<<ext>>"
|
||||
|
||||
formatted = anatomy.format(temp_data)
|
||||
fname_pattern = formatted[template_key]["file"]
|
||||
fname_pattern = re.escape(fname_pattern)
|
||||
|
||||
# Replace comment and version with something we can match with regex
|
||||
replacements = {
|
||||
"<<comment>>": "(.+)",
|
||||
"<<version>>": "[0-9]+",
|
||||
"<<ext>>": any_extension,
|
||||
}
|
||||
for src, dest in replacements.items():
|
||||
fname_pattern = fname_pattern.replace(re.escape(src), dest)
|
||||
|
||||
# Match from beginning to end of string to be safe
|
||||
fname_pattern = "^{}$".format(fname_pattern)
|
||||
|
||||
self.fname_regex = re.compile(fname_pattern)
|
||||
|
||||
def parse_comment(self, filepath):
|
||||
"""Parse the {comment} part from a filename"""
|
||||
if not self.fname_regex:
|
||||
return
|
||||
|
||||
fname = os.path.basename(filepath)
|
||||
match = self.fname_regex.match(fname)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
class SubversionLineEdit(QtWidgets.QWidget):
|
||||
"""QLineEdit with QPushButton for drop down selection of list of strings"""
|
||||
def __init__(self, parent=None):
|
||||
super(SubversionLineEdit, self).__init__(parent=parent)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
|
||||
self._input = PlaceholderLineEdit()
|
||||
self._button = QtWidgets.QPushButton("")
|
||||
self._button.setFixedWidth(18)
|
||||
self._menu = QtWidgets.QMenu(self)
|
||||
self._button.setMenu(self._menu)
|
||||
|
||||
layout.addWidget(self._input)
|
||||
layout.addWidget(self._button)
|
||||
|
||||
@property
|
||||
def input(self):
|
||||
return self._input
|
||||
|
||||
def set_values(self, values):
|
||||
self._update(values)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
self._menu.exec_()
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self._input.setText(action.text())
|
||||
|
||||
def _update(self, values):
|
||||
"""Create optional predefined subset names
|
||||
|
||||
Args:
|
||||
default_names(list): all predefined names
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
menu = self._menu
|
||||
button = self._button
|
||||
|
||||
state = any(values)
|
||||
button.setEnabled(state)
|
||||
if state is False:
|
||||
return
|
||||
|
||||
# Include an empty string
|
||||
values = [""] + sorted(values)
|
||||
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
for name in values:
|
||||
action = group.addAction(name)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
|
||||
|
||||
class NameWindow(QtWidgets.QDialog):
|
||||
"""Name Window to define a unique filename inside a root folder
|
||||
|
||||
|
|
@ -59,56 +238,7 @@ class NameWindow(QtWidgets.QDialog):
|
|||
# Fallback to active session
|
||||
session = api.Session
|
||||
|
||||
# Set work file data for template formatting
|
||||
asset_name = session["AVALON_ASSET"]
|
||||
task_name = session["AVALON_TASK"]
|
||||
project_doc = io.find_one(
|
||||
{"type": "project"},
|
||||
{
|
||||
"name": True,
|
||||
"data.code": True,
|
||||
"config.tasks": True,
|
||||
}
|
||||
)
|
||||
|
||||
asset_doc = io.find_one(
|
||||
{
|
||||
"type": "asset",
|
||||
"name": asset_name
|
||||
},
|
||||
{
|
||||
"data.tasks": True,
|
||||
"data.parents": True
|
||||
}
|
||||
)
|
||||
|
||||
task_type = asset_doc["data"]["tasks"].get(task_name, {}).get("type")
|
||||
|
||||
project_task_types = project_doc["config"]["tasks"]
|
||||
task_short = project_task_types.get(task_type, {}).get("short_name")
|
||||
|
||||
asset_parents = asset_doc["data"]["parents"]
|
||||
parent_name = project_doc["name"]
|
||||
if asset_parents:
|
||||
parent_name = asset_parents[-1]
|
||||
|
||||
self.data = {
|
||||
"project": {
|
||||
"name": project_doc["name"],
|
||||
"code": project_doc["data"].get("code")
|
||||
},
|
||||
"asset": asset_name,
|
||||
"task": {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_short,
|
||||
},
|
||||
"parent": parent_name,
|
||||
"version": 1,
|
||||
"user": getpass.getuser(),
|
||||
"comment": "",
|
||||
"ext": None
|
||||
}
|
||||
self.data = build_workfile_data(session)
|
||||
|
||||
# Store project anatomy
|
||||
self.anatomy = anatomy
|
||||
|
|
@ -151,8 +281,8 @@ class NameWindow(QtWidgets.QDialog):
|
|||
preview_label = QtWidgets.QLabel("Preview filename", inputs_widget)
|
||||
|
||||
# Subversion input
|
||||
subversion_input = PlaceholderLineEdit(inputs_widget)
|
||||
subversion_input.setPlaceholderText("Will be part of filename.")
|
||||
subversion = SubversionLineEdit(inputs_widget)
|
||||
subversion.input.setPlaceholderText("Will be part of filename.")
|
||||
|
||||
# Extensions combobox
|
||||
ext_combo = QtWidgets.QComboBox(inputs_widget)
|
||||
|
|
@ -173,9 +303,27 @@ class NameWindow(QtWidgets.QDialog):
|
|||
|
||||
# Add subversion only if template contains `{comment}`
|
||||
if "{comment}" in self.template:
|
||||
inputs_layout.addRow("Subversion:", subversion_input)
|
||||
inputs_layout.addRow("Subversion:", subversion)
|
||||
|
||||
# Detect whether a {comment} is in the current filename - if so,
|
||||
# preserve it by default and set it in the comment/subversion field
|
||||
current_filepath = self.host.current_file()
|
||||
if current_filepath:
|
||||
# We match the current filename against the current session
|
||||
# instead of the session where the user is saving to.
|
||||
current_data = build_workfile_data(api.Session)
|
||||
matcher = CommentMatcher(anatomy, template_key, current_data)
|
||||
comment = matcher.parse_comment(current_filepath)
|
||||
if comment:
|
||||
log.info("Detected subversion comment: {}".format(comment))
|
||||
self.data["comment"] = comment
|
||||
subversion.input.setText(comment)
|
||||
|
||||
existing_comments = self.get_existing_comments()
|
||||
subversion.set_values(existing_comments)
|
||||
|
||||
else:
|
||||
subversion_input.setVisible(False)
|
||||
subversion.setVisible(False)
|
||||
inputs_layout.addRow("Extension:", ext_combo)
|
||||
inputs_layout.addRow("Preview:", preview_label)
|
||||
|
||||
|
|
@ -190,7 +338,7 @@ class NameWindow(QtWidgets.QDialog):
|
|||
self.on_version_checkbox_changed
|
||||
)
|
||||
|
||||
subversion_input.textChanged.connect(self.on_comment_changed)
|
||||
subversion.input.textChanged.connect(self.on_comment_changed)
|
||||
ext_combo.currentIndexChanged.connect(self.on_extension_changed)
|
||||
|
||||
btn_ok.pressed.connect(self.on_ok_pressed)
|
||||
|
|
@ -201,7 +349,7 @@ class NameWindow(QtWidgets.QDialog):
|
|||
|
||||
# Force default focus to comment, some hosts didn't automatically
|
||||
# apply focus to this line edit (e.g. Houdini)
|
||||
subversion_input.setFocus()
|
||||
subversion.input.setFocus()
|
||||
|
||||
# Store widgets
|
||||
self.btn_ok = btn_ok
|
||||
|
|
@ -212,12 +360,32 @@ class NameWindow(QtWidgets.QDialog):
|
|||
self.last_version_check = last_version_check
|
||||
|
||||
self.preview_label = preview_label
|
||||
self.subversion_input = subversion_input
|
||||
self.subversion = subversion
|
||||
self.ext_combo = ext_combo
|
||||
self._ext_delegate = ext_delegate
|
||||
|
||||
self.refresh()
|
||||
|
||||
def get_existing_comments(self):
|
||||
|
||||
matcher = CommentMatcher(self.anatomy, self.template_key, self.data)
|
||||
host_extensions = set(self.host.file_extensions())
|
||||
comments = set()
|
||||
if os.path.isdir(self.root):
|
||||
for fname in os.listdir(self.root):
|
||||
if not os.path.isfile(os.path.join(self.root, fname)):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(fname)[-1]
|
||||
if ext not in host_extensions:
|
||||
continue
|
||||
|
||||
comment = matcher.parse_comment(fname)
|
||||
if comment:
|
||||
comments.add(comment)
|
||||
|
||||
return list(comments)
|
||||
|
||||
def on_version_spinbox_changed(self, value):
|
||||
self.data["version"] = value
|
||||
self.refresh()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue