Merge branch 'develop' into settings_search

This commit is contained in:
Jakub Trllo 2022-02-22 11:34:15 +01:00 committed by GitHub
commit b1cc2b2da4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
548 changed files with 27631 additions and 391079 deletions

View file

@ -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"],

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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']

View file

@ -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)

View file

@ -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*

View file

@ -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):

View file

@ -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):

View file

@ -16,8 +16,6 @@ from . import views
from maya import cmds
MODELINDEX = QtCore.QModelIndex()
class AssetOutliner(QtWidgets.QWidget):
refreshed = QtCore.Signal()

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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]))

View file

@ -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",

View 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

View file

@ -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)

View file

@ -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:

View 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

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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:

View file

@ -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():

View file

@ -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()

View file

@ -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)

View file

@ -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:

View file

@ -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"

View 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

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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",
)

View file

@ -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)

View file

@ -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:

View 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()

View 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

View file

@ -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:

View file

@ -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))

View file

@ -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",
)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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`

View file

@ -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()