Merge branch 'develop' into feature/OP-2416_Launcher-tool-model

This commit is contained in:
Jakub Trllo 2022-02-22 16:31:29 +01:00
commit 67d3b6cbe5
457 changed files with 20844 additions and 389957 deletions

View file

@ -9,7 +9,11 @@ import appdirs
from Qt import QtCore, QtGui
from avalon.vendor import qtawesome
from avalon import api
from openpype.lib import ApplicationManager, JSONSettingRegistry
from openpype.lib import JSONSettingRegistry
from openpype.lib.applications import (
CUSTOM_LAUNCH_APP_GROUPS,
ApplicationManager
)
from openpype.tools.utils.lib import DynamicQThread
from openpype.tools.utils.assets_widget import (
AssetModel,
@ -90,6 +94,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),
@ -331,7 +338,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

@ -18,6 +18,7 @@ from .models import (
LauncherTaskModel,
LauncherTasksProxyModel
)
from .actions import ApplicationAction
from .constants import (
ACTION_ROLE,
GROUP_ROLE,
@ -313,10 +314,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

@ -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()
@ -142,6 +152,8 @@ class Controller(QtCore.QObject):
self.instance_toggled.connect(self._on_instance_toggled)
self._main_thread_processor = MainThreadProcess()
self._current_state = ""
def reset_variables(self):
self.log.debug("Resetting pyblish context variables")
@ -149,6 +161,7 @@ class Controller(QtCore.QObject):
self.is_running = False
self.stopped = False
self.errored = False
self._current_state = ""
# Active producer of pairs
self.pair_generator = None
@ -157,7 +170,6 @@ class Controller(QtCore.QObject):
# Orders which changes GUI
# - passing collectors order disables plugin/instance toggle
self.collectors_order = None
self.collect_state = 0
# - passing validators order disables validate button and gives ability
@ -166,11 +178,8 @@ class Controller(QtCore.QObject):
self.validated = False
# Get collectors and validators order
self.order_groups.reset()
plugin_groups = self.order_groups.groups()
plugin_groups_keys = list(plugin_groups.keys())
self.collectors_order = plugin_groups_keys[0]
self.validators_order = self.order_groups.validation_order()
plugin_groups_keys = list(self.order_groups.groups.keys())
self.validators_order = self.order_groups.validation_order
next_group_order = None
if len(plugin_groups_keys) > 1:
next_group_order = plugin_groups_keys[1]
@ -181,13 +190,18 @@ class Controller(QtCore.QObject):
"stop_on_validation": False,
# Used?
"last_plugin_order": None,
"current_group_order": self.collectors_order,
"current_group_order": plugin_groups_keys[0],
"next_group_order": next_group_order,
"nextOrder": None,
"ordersWithError": set()
}
self._set_state_by_order()
self.log.debug("Reset of pyblish context variables done")
@property
def current_state(self):
return self._current_state
def presets_by_hosts(self):
# Get global filters as base
presets = get_project_settings(os.environ['AVALON_PROJECT']) or {}
@ -283,6 +297,9 @@ class Controller(QtCore.QObject):
def on_published(self):
if self.is_running:
self.is_running = False
self._current_state = (
"Published" if not self.errored else "Published, with errors"
)
self.was_finished.emit()
self._main_thread_processor.stop()
@ -345,7 +362,7 @@ class Controller(QtCore.QObject):
new_current_group_order = self.processing["next_group_order"]
if new_current_group_order is not None:
current_next_order_found = False
for order in self.order_groups.groups().keys():
for order in self.order_groups.groups.keys():
if current_next_order_found:
new_next_group_order = order
break
@ -360,6 +377,10 @@ class Controller(QtCore.QObject):
if self.collect_state == 0:
self.collect_state = 1
self._current_state = (
"Ready" if not self.errored else
"Collected, with errors"
)
self.switch_toggleability.emit(True)
self.passed_group.emit(current_group_order)
yield IterationBreak("Collected")
@ -367,6 +388,11 @@ class Controller(QtCore.QObject):
else:
self.passed_group.emit(current_group_order)
if self.errored:
self._current_state = (
"Stopped, due to errors" if not
self.processing["stop_on_validation"] else
"Validated, with errors"
)
yield IterationBreak("Last group errored")
if self.collect_state == 1:
@ -376,17 +402,23 @@ class Controller(QtCore.QObject):
if not self.validated and plugin.order > self.validators_order:
self.validated = True
if self.processing["stop_on_validation"]:
self._current_state = (
"Validated" if not self.errored else
"Validated, with errors"
)
yield IterationBreak("Validated")
# Stop if was stopped
if self.stopped:
self.stopped = False
self._current_state = "Paused"
yield IterationBreak("Stopped")
# check test if will stop
self.processing["nextOrder"] = plugin.order
message = self.test(**self.processing)
if message:
self._current_state = "Paused"
yield IterationBreak("Stopped due to \"{}\"".format(message))
self.processing["last_plugin_order"] = plugin.order
@ -416,6 +448,7 @@ class Controller(QtCore.QObject):
# Stop if was stopped
if self.stopped:
self.stopped = False
self._current_state = "Paused"
yield IterationBreak("Stopped")
yield (plugin, instance)
@ -526,20 +559,27 @@ class Controller(QtCore.QObject):
MainThreadItem(on_next)
)
def _set_state_by_order(self):
order = self.processing["current_group_order"]
self._current_state = self.order_groups.groups[order]["state"]
def collect(self):
""" Iterate and process Collect plugins
- load_plugins method is launched again when finished
"""
self._set_state_by_order()
self._main_thread_processor.process(self._start_collect)
self._main_thread_processor.start()
def validate(self):
""" Process plugins to validations_order value."""
self._set_state_by_order()
self._main_thread_processor.process(self._start_validate)
self._main_thread_processor.start()
def publish(self):
""" Iterate and process all remaining plugins."""
self._set_state_by_order()
self._main_thread_processor.process(self._start_publish)
self._main_thread_processor.start()

View file

@ -428,12 +428,12 @@ class PluginModel(QtGui.QStandardItemModel):
self.clear()
def append(self, plugin):
plugin_groups = self.controller.order_groups.groups()
plugin_groups = self.controller.order_groups.groups
label = None
order = None
for _order, _label in reversed(plugin_groups.items()):
for _order, item in reversed(plugin_groups.items()):
if _order is None or plugin.order < _order:
label = _label
label = item["label"]
order = _order
else:
break

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

@ -95,224 +95,44 @@ def collect_families_from_instances(instances, only_active=False):
class OrderGroups:
# Validator order can be set with environment "PYBLISH_VALIDATION_ORDER"
# - this variable sets when validation button will hide and proecssing
# of validation will end with ability to continue in process
default_validation_order = pyblish.api.ValidatorOrder + 0.5
# Group range can be set with environment "PYBLISH_GROUP_RANGE"
default_group_range = 1
# Group string can be set with environment "PYBLISH_GROUP_SETTING"
default_groups = {
pyblish.api.CollectorOrder + 0.5: "Collect",
pyblish.api.ValidatorOrder + 0.5: "Validate",
pyblish.api.ExtractorOrder + 0.5: "Extract",
pyblish.api.IntegratorOrder + 0.5: "Integrate",
None: "Other"
}
# *** This example should have same result as is `default_groups` if
# `group_range` is set to "1"
__groups_str_example__ = (
# half of `group_range` is added to 0 because number means it is Order
"0=Collect"
# if `<` is before than it means group range is not used
# but is expected that number is already max
",<1.5=Validate"
# "Extractor" will be used in range `<1.5; 2.5)`
",<2.5=Extract"
",<3.5=Integrate"
# "Other" if number is not set than all remaining plugins are in
# - in this case Other's range is <3.5; infinity)
",Other"
)
_groups = None
_validation_order = None
_group_range = None
def __init__(
self, group_str=None, group_range=None, validation_order=None
):
super(OrderGroups, self).__init__()
# Override class methods with object methods
self.groups = self._object_groups
self.validation_order = self._object_validation_order
self.group_range = self._object_group_range
self.reset = self._object_reset
# set
if group_range is not None:
self._group_range = self.parse_group_range(
group_range
)
if group_str is not None:
self._groups = self.parse_group_str(
group_str
)
if validation_order is not None:
self._validation_order = self.parse_validation_order(
validation_order
)
@staticmethod
def _groups_method(obj):
if obj._groups is None:
obj._groups = obj.parse_group_str(
group_range=obj.group_range()
)
return obj._groups
@staticmethod
def _reset_method(obj):
obj._groups = None
obj._validation_order = None
obj._group_range = None
@classmethod
def reset(cls):
return cls._reset_method(cls)
def _object_reset(self):
return self._reset_method(self)
@classmethod
def groups(cls):
return cls._groups_method(cls)
def _object_groups(self):
return self._groups_method(self)
@staticmethod
def _validation_order_method(obj):
if obj._validation_order is None:
obj._validation_order = obj.parse_validation_order(
group_range=obj.group_range()
)
return obj._validation_order
@classmethod
def validation_order(cls):
return cls._validation_order_method(cls)
def _object_validation_order(self):
return self._validation_order_method(self)
@staticmethod
def _group_range_method(obj):
if obj._group_range is None:
obj._group_range = obj.parse_group_range()
return obj._group_range
@classmethod
def group_range(cls):
return cls._group_range_method(cls)
def _object_group_range(self):
return self._group_range_method(self)
@staticmethod
def sort_groups(_groups_dict):
sorted_dict = collections.OrderedDict()
# make sure won't affect any dictionary as pointer
groups_dict = copy.deepcopy(_groups_dict)
last_order = None
if None in groups_dict:
last_order = groups_dict.pop(None)
for key in sorted(groups_dict):
sorted_dict[key] = groups_dict[key]
if last_order is not None:
sorted_dict[None] = last_order
return sorted_dict
@staticmethod
def parse_group_str(groups_str=None, group_range=None):
if groups_str is None:
groups_str = os.environ.get("PYBLISH_GROUP_SETTING")
if groups_str is None:
return OrderGroups.sort_groups(OrderGroups.default_groups)
items = groups_str.split(",")
groups = {}
for item in items:
if "=" not in item:
order = None
label = item
else:
order, label = item.split("=")
order = order.strip()
if not order:
order = None
elif order.startswith("<"):
order = float(order.replace("<", ""))
else:
if group_range is None:
group_range = OrderGroups.default_group_range
print(
"Using default Plugin group range \"{}\".".format(
OrderGroups.default_group_range
)
)
order = float(order) + float(group_range) / 2
if order in groups:
print((
"Order \"{}\" is registered more than once."
" Using first found."
).format(str(order)))
continue
groups[order] = label
return OrderGroups.sort_groups(groups)
@staticmethod
def parse_validation_order(validation_order_value=None, group_range=None):
if validation_order_value is None:
validation_order_value = os.environ.get("PYBLISH_VALIDATION_ORDER")
if validation_order_value is None:
return OrderGroups.default_validation_order
if group_range is None:
group_range = OrderGroups.default_group_range
group_range_half = float(group_range) / 2
if isinstance(validation_order_value, numbers.Integral):
return validation_order_value + group_range_half
if validation_order_value.startswith("<"):
validation_order_value = float(
validation_order_value.replace("<", "")
)
else:
validation_order_value = (
float(validation_order_value)
+ group_range_half
)
return validation_order_value
@staticmethod
def parse_group_range(group_range=None):
if group_range is None:
group_range = os.environ.get("PYBLISH_GROUP_RANGE")
if group_range is None:
return OrderGroups.default_group_range
if isinstance(group_range, numbers.Integral):
return group_range
return float(group_range)
validation_order = pyblish.api.ValidatorOrder + 0.5
groups = collections.OrderedDict((
(
pyblish.api.CollectorOrder + 0.5,
{
"label": "Collect",
"state": "Collecting.."
}
),
(
pyblish.api.ValidatorOrder + 0.5,
{
"label": "Validate",
"state": "Validating.."
}
),
(
pyblish.api.ExtractorOrder + 0.5,
{
"label": "Extract",
"state": "Extracting.."
}
),
(
pyblish.api.IntegratorOrder + 0.5,
{
"label": "Integrate",
"state": "Integrating.."
}
),
(
None,
{
"label": "Other",
"state": "Finishing.."
}
)
))
def env_variable_to_bool(env_key, default=False):

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
)
@ -298,34 +296,6 @@ class Window(QtWidgets.QDialog):
self.main_layout.setSpacing(0)
self.main_layout.addWidget(main_widget)
# Display info
info_effect = QtWidgets.QGraphicsOpacityEffect(footer_info)
footer_info.setGraphicsEffect(info_effect)
on = QtCore.QPropertyAnimation(info_effect, b"opacity")
on.setDuration(0)
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)
fade.setEndValue(0.0)
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)
"""Setup
Widgets are referred to in CSS via their object-name. We
@ -459,6 +429,8 @@ class Window(QtWidgets.QDialog):
self.footer_button_validate = footer_button_validate
self.footer_button_play = footer_button_play
self.footer_info = footer_info
self.overview_instance_view = overview_instance_view
self.overview_plugin_view = overview_plugin_view
self.plugin_model = plugin_model
@ -468,8 +440,6 @@ class Window(QtWidgets.QDialog):
self.presets_button = presets_button
self.animation_info_msg = animation_info_msg
self.terminal_model = terminal_model
self.terminal_proxy = terminal_proxy
self.terminal_view = terminal_view
@ -995,6 +965,8 @@ class Window(QtWidgets.QDialog):
self.footer_button_stop.setEnabled(True)
self.footer_button_play.setEnabled(False)
self._update_state()
def on_passed_group(self, order):
for group_item in self.instance_model.group_items.values():
group_index = self.instance_sort_proxy.mapFromSource(
@ -1023,9 +995,13 @@ class Window(QtWidgets.QDialog):
{GroupStates.HasFinished: True},
Roles.PublishFlagsRole
)
self.overview_plugin_view.setAnimated(False)
self.overview_plugin_view.collapse(group_index)
self._update_state()
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)
@ -1049,6 +1025,11 @@ class Window(QtWidgets.QDialog):
)
self.button_suspend_logs.setEnabled(suspend_log_bool)
self._update_state()
if not self.isVisible():
self.setVisible(True)
def on_was_skipped(self, plugin):
plugin_item = self.plugin_model.plugin_items[plugin.id]
plugin_item.setData(
@ -1057,6 +1038,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)
@ -1090,6 +1072,7 @@ class Window(QtWidgets.QDialog):
)
self.update_compatibility()
self._update_state()
def on_was_processed(self, result):
existing_ids = set(self.instance_model.instance_items.keys())
@ -1120,6 +1103,9 @@ class Window(QtWidgets.QDialog):
plugin_item, instance_item
)
if not self.isVisible():
self.setVisible(True)
# -------------------------------------------------------------------------
#
# Functions
@ -1168,6 +1154,8 @@ class Window(QtWidgets.QDialog):
self.controller.validate()
self._update_state()
def publish(self):
self.info(self.tr("Preparing publish.."))
self.footer_button_stop.setEnabled(True)
@ -1179,6 +1167,8 @@ class Window(QtWidgets.QDialog):
self.controller.publish()
self._update_state()
def act(self, plugin_item, action):
self.info("%s %s.." % (self.tr("Preparing"), action))
@ -1293,6 +1283,9 @@ class Window(QtWidgets.QDialog):
#
# -------------------------------------------------------------------------
def _update_state(self):
self.footer_info.setText(self.controller.current_state)
def info(self, message):
"""Print user-facing information
@ -1300,22 +1293,15 @@ class Window(QtWidgets.QDialog):
message (str): Text message for the user
"""
info = self.findChild(QtWidgets.QLabel, "FooterInfo")
info.setText(message)
# Include message in terminal
self.terminal_model.append([{
"label": message,
"type": "info"
}])
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

@ -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,
@ -86,8 +91,24 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
state_changed = QtCore.Signal()
saved = QtCore.Signal(QtWidgets.QWidget)
restart_required_trigger = QtCore.Signal()
reset_started = QtCore.Signal()
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)
@ -98,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 = []
@ -183,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
@ -307,22 +381,23 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
"""Change path of widget based on category full path."""
pass
def set_path(self, path):
self.breadcrumbs_widget.set_path(path)
def change_path(self, path):
"""Change path and go to widget."""
self.breadcrumbs_bar.change_path(path)
def _add_developer_ui(self, footer_layout):
modify_defaults_widget = QtWidgets.QWidget()
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
def set_path(self, path):
"""Called from clicked widget."""
self.breadcrumbs_bar.set_path(path)
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
@ -361,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
@ -420,15 +496,10 @@ 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()
self.set_state(CategoryState.Working)
self._on_reset_start()
@ -444,6 +515,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
widget.deleteLater()
dialog = None
self._updating_root = True
source_version = ""
try:
self._create_root_entity()
@ -459,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)
@ -502,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:
@ -509,6 +604,37 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._on_reset_crash()
else:
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)
@ -521,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:
@ -565,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
@ -584,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()
@ -594,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)
@ -620,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:
@ -638,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
@ -651,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()
@ -684,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()
@ -696,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)
@ -754,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

@ -0,0 +1,186 @@
import re
import collections
from Qt import QtCore, QtWidgets, QtGui
ENTITY_LABEL_ROLE = QtCore.Qt.UserRole + 1
ENTITY_PATH_ROLE = QtCore.Qt.UserRole + 2
def get_entity_children(entity):
# TODO find better way how to go through all children
if hasattr(entity, "values"):
return entity.values()
return []
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filters recursively to regex in all columns"""
def __init__(self):
super(RecursiveSortFilterProxyModel, self).__init__()
# Note: Recursive filtering was introduced in Qt 5.10.
self.setRecursiveFilteringEnabled(True)
def filterAcceptsRow(self, row, parent):
if not parent.isValid():
return False
regex = self.filterRegExp()
if not regex.isEmpty() and regex.isValid():
pattern = regex.pattern()
compiled_regex = re.compile(pattern)
source_model = self.sourceModel()
# Check current index itself in all columns
source_index = source_model.index(row, 0, parent)
if source_index.isValid():
for role in (ENTITY_PATH_ROLE, ENTITY_LABEL_ROLE):
value = source_model.data(source_index, role)
if value and compiled_regex.search(value):
return True
return False
return super(
RecursiveSortFilterProxyModel, self
).filterAcceptsRow(row, parent)
class SearchEntitiesDialog(QtWidgets.QDialog):
path_clicked = QtCore.Signal(str)
def __init__(self, parent):
super(SearchEntitiesDialog, self).__init__(parent=parent)
self.setWindowTitle("Search Settings")
filter_edit = QtWidgets.QLineEdit(self)
filter_edit.setPlaceholderText("Search...")
model = EntityTreeModel()
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
view = QtWidgets.QTreeView(self)
view.setAllColumnsShowFocus(True)
view.setSortingEnabled(True)
view.setModel(proxy)
model.setColumnCount(3)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_edit)
layout.addWidget(view)
filter_changed_timer = QtCore.QTimer()
filter_changed_timer.setInterval(200)
view.selectionModel().selectionChanged.connect(
self._on_selection_change
)
filter_changed_timer.timeout.connect(self._on_filter_timer)
filter_edit.textChanged.connect(self._on_filter_changed)
self._filter_edit = filter_edit
self._model = model
self._proxy = proxy
self._view = view
self._filter_changed_timer = filter_changed_timer
self._first_show = True
def set_root_entity(self, entity):
self._model.set_root_entity(entity)
self._view.resizeColumnToContents(0)
def showEvent(self, event):
super(SearchEntitiesDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
self.resize(700, 500)
def _on_filter_changed(self, txt):
self._filter_changed_timer.start()
def _on_filter_timer(self):
text = self._filter_edit.text()
self._proxy.setFilterRegExp(text)
# WARNING This expanding and resizing is relatively slow.
self._view.expandAll()
self._view.resizeColumnToContents(0)
def _on_selection_change(self):
current = self._view.currentIndex()
path = current.data(ENTITY_PATH_ROLE)
self.path_clicked.emit(path)
class EntityTreeModel(QtGui.QStandardItemModel):
def __init__(self, *args, **kwargs):
super(EntityTreeModel, self).__init__(*args, **kwargs)
self.setColumnCount(3)
def data(self, index, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
col = index.column()
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
if col == 0:
pass
elif col == 1:
role = ENTITY_LABEL_ROLE
elif col == 2:
role = ENTITY_PATH_ROLE
if col > 0:
index = self.index(index.row(), 0, index.parent())
return super(EntityTreeModel, self).data(index, role)
def flags(self, index):
if index.column() > 0:
index = self.index(index.row(), 0, index.parent())
return super(EntityTreeModel, self).flags(index)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Key"
elif section == 1:
return "Label"
elif section == 2:
return "Path"
return ""
return super(EntityTreeModel, self).headerData(
section, orientation, role
)
def set_root_entity(self, root_entity):
parent = self.invisibleRootItem()
parent.removeRows(0, parent.rowCount())
if not root_entity:
return
# We don't want to see the root entity so we directly add its children
fill_queue = collections.deque()
fill_queue.append((root_entity, parent))
cols = self.columnCount()
while fill_queue:
parent_entity, parent_item = fill_queue.popleft()
child_items = []
for child in get_entity_children(parent_entity):
label = child.label
path = child.path
key = path.split("/")[-1]
item = QtGui.QStandardItem(key)
item.setEditable(False)
item.setData(label, ENTITY_LABEL_ROLE)
item.setData(path, ENTITY_PATH_ROLE)
item.setColumnCount(cols)
child_items.append(item)
fill_queue.append((child, item))
if child_items:
parent_item.appendRows(child_items)

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,12 @@ 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
from openpype.lib import is_admin_password_required
@ -34,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)
@ -54,19 +59,31 @@ class MainWidget(QtWidgets.QWidget):
self.setLayout(layout)
search_dialog = SearchEntitiesDialog(self)
self._shadow_widget = ShadowWidget("Working...", self)
self._shadow_widget.setVisible(False)
header_tab_widget.currentChanged.connect(self._on_tab_changed)
search_dialog.path_clicked.connect(self._on_search_path_clicked)
for tab_widget in tab_widgets:
tab_widget.saved.connect(self._on_tab_save)
tab_widget.state_changed.connect(self._on_state_change)
tab_widget.restart_required_trigger.connect(
self._on_restart_required
)
tab_widget.reset_started.connect(self._on_reset_started)
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
def _on_tab_save(self, source_widget):
for tab_widget in self.tab_widgets:
@ -100,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:
@ -150,6 +179,21 @@ class MainWidget(QtWidgets.QWidget):
for tab_widget in self.tab_widgets:
tab_widget.reset()
def _update_search_dialog(self, clear=False):
if self._search_dialog.isVisible():
entity = None
if not clear:
widget = self._header_tab_widget.currentWidget()
entity = widget.entity
self._search_dialog.set_root_entity(entity)
def _on_tab_changed(self):
self._update_search_dialog()
def _on_search_path_clicked(self, path):
widget = self._header_tab_widget.currentWidget()
widget.change_path(path)
def _on_restart_required(self):
# Don't show dialog if there are not registered slots for
# `trigger_restart` signal.
@ -164,3 +208,26 @@ class MainWidget(QtWidgets.QWidget):
result = dialog.exec_()
if result == 1:
self.trigger_restart.emit()
def _on_reset_started(self):
widget = self.sender()
current_widget = self._header_tab_widget.currentWidget()
if current_widget is widget:
self._update_search_dialog(True)
def _on_reset_finished(self):
widget = self.sender()
current_widget = self._header_tab_widget.currentWidget()
if current_widget is widget:
self._update_search_dialog()
def keyPressEvent(self, event):
if event.matches(QtGui.QKeySequence.Find):
# todo: search in all widgets (or in active)?
widget = self._header_tab_widget.currentWidget()
self._search_dialog.show()
self._search_dialog.set_root_entity(widget.entity)
event.accept()
return
return super(MainWidget, self).keyPressEvent(event)

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

@ -10,7 +10,10 @@ from .widgets import (
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 (
@ -29,6 +32,9 @@ __all__ = (
"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
@ -381,9 +381,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()
@ -393,12 +395,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

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,

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()
@ -558,9 +726,9 @@ class FilesWidget(QtWidgets.QWidget):
self.file_opened.emit()
def save_changes_prompt(self):
self._messagebox = messagebox = QtWidgets.QMessageBox()
messagebox.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self)
messagebox.setWindowFlags(messagebox.windowFlags() |
QtCore.Qt.FramelessWindowHint)
messagebox.setIcon(messagebox.Warning)
messagebox.setWindowTitle("Unsaved Changes!")
messagebox.setText(
@ -571,10 +739,6 @@ class FilesWidget(QtWidgets.QWidget):
messagebox.Yes | messagebox.No | messagebox.Cancel
)
# Parenting the QMessageBox to the Widget seems to crash
# so we skip parenting and explicitly apply the stylesheet.
messagebox.setStyle(self.style())
result = messagebox.exec_()
if result == messagebox.Yes:
return True