Merge pull request #1396 from pypeclub/feature/project_manager

This commit is contained in:
Milan Kolar 2021-05-19 10:06:23 +02:00 committed by GitHub
commit 2b8233de53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 4038 additions and 8 deletions

View file

@ -224,6 +224,11 @@ def launch(app, project, asset, task,
PypeCommands().run_application(app, project, asset, task, tools, arguments)
@main.command(context_settings={"ignore_unknown_options": True})
def projectmanager():
PypeCommands().launch_project_manager()
@main.command(
context_settings=dict(
ignore_unknown_options=True,

View file

@ -58,6 +58,10 @@ from .python_module_tools import (
)
from .avalon_context import (
CURRENT_DOC_SCHEMAS,
PROJECT_NAME_ALLOWED_SYMBOLS,
PROJECT_NAME_REGEX,
create_project,
is_latest,
any_outdated,
get_asset,
@ -163,6 +167,10 @@ __all__ = [
"recursive_bases_from_class",
"classes_from_module",
"CURRENT_DOC_SCHEMAS",
"PROJECT_NAME_ALLOWED_SYMBOLS",
"PROJECT_NAME_REGEX",
"create_project",
"is_latest",
"any_outdated",
"get_asset",

View file

@ -17,6 +17,99 @@ avalon = None
log = logging.getLogger("AvalonContext")
CURRENT_DOC_SCHEMAS = {
"project": "openpype:project-3.0",
"asset": "openpype:asset-3.0",
"config": "openpype:config-2.0"
}
PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_"
PROJECT_NAME_REGEX = re.compile(
"^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS)
)
def create_project(
project_name, project_code, library_project=False, dbcon=None
):
"""Create project using OpenPype settings.
This project creation function is not validating project document on
creation. It is because project document is created blindly with only
minimum required information about project which is it's name, code, type
and schema.
Entered project name must be unique and project must not exist yet.
Args:
project_name(str): New project name. Should be unique.
project_code(str): Project's code should be unique too.
library_project(bool): Project is library project.
dbcon(AvalonMongoDB): Object of connection to MongoDB.
Raises:
ValueError: When project name already exists in MongoDB.
Returns:
dict: Created project document.
"""
from openpype.settings import ProjectSettings, SaveWarningExc
from avalon.api import AvalonMongoDB
from avalon.schema import validate
if dbcon is None:
dbcon = AvalonMongoDB()
if not PROJECT_NAME_REGEX.match(project_name):
raise ValueError((
"Project name \"{}\" contain invalid characters"
).format(project_name))
database = dbcon.database
project_doc = database[project_name].find_one(
{"type": "project"},
{"name": 1}
)
if project_doc:
raise ValueError("Project with name \"{}\" already exists".format(
project_name
))
project_doc = {
"type": "project",
"name": project_name,
"data": {
"code": project_code,
"library_project": library_project
},
"schema": CURRENT_DOC_SCHEMAS["project"]
}
# Insert document with basic data
database[project_name].insert_one(project_doc)
# Load ProjectSettings for the project and save it to store all attributes
# and Anatomy
try:
project_settings_entity = ProjectSettings(project_name)
project_settings_entity.save()
except SaveWarningExc as exc:
print(str(exc))
except Exception:
database[project_name].delete_one({"type": "project"})
raise
project_doc = database[project_name].find_one({"type": "project"})
try:
# Validate created project document
validate(project_doc)
except Exception:
# Remove project if is not valid
database[project_name].delete_one({"type": "project"})
raise
return project_doc
def with_avalon(func):
@functools.wraps(func)
def wrap_avalon(*args, **kwargs):

View file

@ -26,9 +26,7 @@ from openpype.modules.ftrack.lib import (
BaseEvent
)
from openpype.modules.ftrack.lib.avalon_sync import (
EntitySchemas
)
from openpype.lib import CURRENT_DOC_SCHEMAS
class SyncToAvalonEvent(BaseEvent):
@ -1128,7 +1126,7 @@ class SyncToAvalonEvent(BaseEvent):
"_id": mongo_id,
"name": name,
"type": "asset",
"schema": EntitySchemas["asset"],
"schema": CURRENT_DOC_SCHEMAS["asset"],
"parent": proj["_id"],
"data": {
"ftrackId": ftrack_ent["id"],

View file

@ -34,7 +34,7 @@ log = Logger.get_logger(__name__)
# Current schemas for avalon types
EntitySchemas = {
CURRENT_DOC_SCHEMAS = {
"project": "openpype:project-3.0",
"asset": "openpype:asset-3.0",
"config": "openpype:config-2.0"
@ -1862,7 +1862,7 @@ class SyncEntitiesFactory:
item["_id"] = new_id
item["parent"] = self.avalon_project_id
item["schema"] = EntitySchemas["asset"]
item["schema"] = CURRENT_DOC_SCHEMAS["asset"]
item["data"]["visualParent"] = avalon_parent
new_id_str = str(new_id)
@ -2003,8 +2003,8 @@ class SyncEntitiesFactory:
project_item["_id"] = new_id
project_item["parent"] = None
project_item["schema"] = EntitySchemas["project"]
project_item["config"]["schema"] = EntitySchemas["config"]
project_item["schema"] = CURRENT_DOC_SCHEMAS["project"]
project_item["config"]["schema"] = CURRENT_DOC_SCHEMAS["config"]
self.ftrack_avalon_mapper[self.ft_project_id] = new_id
self.avalon_ftrack_mapper[new_id] = self.ft_project_id

View file

@ -110,6 +110,12 @@ class PypeCommands:
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)
@staticmethod
def launch_project_manager():
from openpype.tools import project_manager
project_manager.main()
def texture_copy(self, project, asset, path):
pass

View file

@ -0,0 +1,10 @@
from .project_manager import (
ProjectManagerWindow,
main
)
__all__ = (
"ProjectManagerWindow",
"main"
)

View file

@ -0,0 +1,5 @@
from project_manager import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,50 @@
__all__ = (
"IDENTIFIER_ROLE",
"HierarchyView",
"ProjectModel",
"CreateProjectDialog",
"HierarchyModel",
"HierarchySelectionModel",
"BaseItem",
"RootItem",
"ProjectItem",
"AssetItem",
"TaskItem",
"ProjectManagerWindow",
"main"
)
from .constants import (
IDENTIFIER_ROLE
)
from .widgets import CreateProjectDialog
from .view import HierarchyView
from .model import (
ProjectModel,
HierarchyModel,
HierarchySelectionModel,
BaseItem,
RootItem,
ProjectItem,
AssetItem,
TaskItem
)
from .window import ProjectManagerWindow
def main():
import sys
from Qt import QtWidgets
app = QtWidgets.QApplication([])
window = ProjectManagerWindow()
window.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,13 @@
import re
from Qt import QtCore
IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1
DUPLICATED_ROLE = QtCore.Qt.UserRole + 2
HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3
REMOVED_ROLE = QtCore.Qt.UserRole + 4
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6
NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_"
NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$")

View file

@ -0,0 +1,159 @@
from Qt import QtWidgets, QtCore
from .widgets import (
NameTextEdit,
FilterComboBox
)
from .multiselection_combobox import MultiSelectionComboBox
class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate):
@staticmethod
def _q_smart_min_size(editor):
min_size_hint = editor.minimumSizeHint()
size_policy = editor.sizePolicy()
width = 0
height = 0
if size_policy.horizontalPolicy() != QtWidgets.QSizePolicy.Ignored:
if (
size_policy.horizontalPolicy()
& QtWidgets.QSizePolicy.ShrinkFlag
):
width = min_size_hint.width()
else:
width = max(
editor.sizeHint().width(),
min_size_hint.width()
)
if size_policy.verticalPolicy() != QtWidgets.QSizePolicy.Ignored:
if size_policy.verticalPolicy() & QtWidgets.QSizePolicy.ShrinkFlag:
height = min_size_hint.height()
else:
height = max(
editor.sizeHint().height(),
min_size_hint.height()
)
output = QtCore.QSize(width, height).boundedTo(editor.maximumSize())
min_size = editor.minimumSize()
if min_size.width() > 0:
output.setWidth(min_size.width())
if min_size.height() > 0:
output.setHeight(min_size.height())
return output.expandedTo(QtCore.QSize(0, 0))
def updateEditorGeometry(self, editor, option, index):
self.initStyleOption(option, index)
option.showDecorationSelected = editor.style().styleHint(
QtWidgets.QStyle.SH_ItemView_ShowDecorationSelected, None, editor
)
widget = option.widget
style = widget.style() if widget else QtWidgets.QApplication.style()
geo = style.subElementRect(
QtWidgets.QStyle.SE_ItemViewItemText, option, widget
)
delta = self._q_smart_min_size(editor).width() - geo.width()
if delta > 0:
if editor.layoutDirection() == QtCore.Qt.RightToLeft:
geo.adjust(-delta, 0, 0, 0)
else:
geo.adjust(0, 0, delta, 0)
editor.setGeometry(geo)
class NumberDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, minimum, maximum, decimals, *args, **kwargs):
super(NumberDelegate, self).__init__(*args, **kwargs)
self.minimum = minimum
self.maximum = maximum
self.decimals = decimals
def createEditor(self, parent, option, index):
if self.decimals > 0:
editor = QtWidgets.QDoubleSpinBox(parent)
else:
editor = QtWidgets.QSpinBox(parent)
editor.setObjectName("NumberEditor")
editor.setMinimum(self.minimum)
editor.setMaximum(self.maximum)
editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
value = index.data(QtCore.Qt.EditRole)
if value is not None:
try:
if isinstance(value, str):
value = float(value)
editor.setValue(value)
except Exception:
print("Couldn't set invalid value \"{}\"".format(str(value)))
return editor
class NameDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = NameTextEdit(parent)
editor.setObjectName("NameEditor")
value = index.data(QtCore.Qt.EditRole)
if value is not None:
editor.setText(str(value))
return editor
class TypeDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, project_doc_cache, *args, **kwargs):
self._project_doc_cache = project_doc_cache
super(TypeDelegate, self).__init__(*args, **kwargs)
def createEditor(self, parent, option, index):
editor = FilterComboBox(parent)
editor.setObjectName("TypeEditor")
editor.style().polish(editor)
if not self._project_doc_cache.project_doc:
return editor
task_type_defs = self._project_doc_cache.project_doc["config"]["tasks"]
editor.addItems(list(task_type_defs.keys()))
return editor
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.EditRole)
index = editor.findText(value)
if index >= 0:
editor.setCurrentIndex(index)
def setModelData(self, editor, model, index):
editor.value_cleanup()
super(TypeDelegate, self).setModelData(editor, model, index)
class ToolsDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, tools_cache, *args, **kwargs):
self._tools_cache = tools_cache
super(ToolsDelegate, self).__init__(*args, **kwargs)
def createEditor(self, parent, option, index):
editor = MultiSelectionComboBox(parent)
editor.setObjectName("ToolEditor")
if not self._tools_cache.tools_data:
return editor
for key, label in self._tools_cache.tools_data:
editor.addItem(label, key)
return editor
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.EditRole)
editor.set_value(value)
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,215 @@
from Qt import QtCore, QtGui, QtWidgets
class ComboItemDelegate(QtWidgets.QStyledItemDelegate):
"""
Helper styled delegate (mostly based on existing private Qt's
delegate used by the QtWidgets.QComboBox). Used to style the popup like a
list view (e.g windows style).
"""
def paint(self, painter, option, index):
option = QtWidgets.QStyleOptionViewItem(option)
option.showDecorationSelected = True
# option.state &= (
# ~QtWidgets.QStyle.State_HasFocus
# & ~QtWidgets.QStyle.State_MouseOver
# )
super(ComboItemDelegate, self).paint(painter, option, index)
class MultiSelectionComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal()
ignored_keys = {
QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down,
QtCore.Qt.Key_PageDown,
QtCore.Qt.Key_PageUp,
QtCore.Qt.Key_Home,
QtCore.Qt.Key_End
}
def __init__(self, parent=None, **kwargs):
super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)
self.setObjectName("MultiSelectionComboBox")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self._popup_is_shown = False
self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)
self._initial_mouse_pos = None
self._delegate = ComboItemDelegate(self)
self.setItemDelegate(self._delegate)
def mousePressEvent(self, event):
"""Reimplemented."""
self._popup_is_shown = False
super(MultiSelectionComboBox, self).mousePressEvent(event)
if self._popup_is_shown:
self._initial_mouse_pos = self.mapToGlobal(event.pos())
self._block_mouse_release_timer.start(
QtWidgets.QApplication.doubleClickInterval()
)
def showPopup(self):
"""Reimplemented."""
super(MultiSelectionComboBox, self).showPopup()
view = self.view()
view.installEventFilter(self)
view.viewport().installEventFilter(self)
self._popup_is_shown = True
def hidePopup(self):
"""Reimplemented."""
self.view().removeEventFilter(self)
self.view().viewport().removeEventFilter(self)
self._popup_is_shown = False
self._initial_mouse_pos = None
super(MultiSelectionComboBox, self).hidePopup()
self.view().clearFocus()
def _event_popup_shown(self, obj, event):
if not self._popup_is_shown:
return
current_index = self.view().currentIndex()
model = self.model()
if event.type() == QtCore.QEvent.MouseMove:
if (
self.view().isVisible()
and self._initial_mouse_pos is not None
and self._block_mouse_release_timer.isActive()
):
diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos
if diff.manhattanLength() > 9:
self._block_mouse_release_timer.stop()
return
index_flags = current_index.flags()
state = current_index.data(QtCore.Qt.CheckStateRole)
new_state = None
if event.type() == QtCore.QEvent.MouseButtonRelease:
if (
self._block_mouse_release_timer.isActive()
or not current_index.isValid()
or not self.view().isVisible()
or not self.view().rect().contains(event.pos())
or not index_flags & QtCore.Qt.ItemIsSelectable
or not index_flags & QtCore.Qt.ItemIsEnabled
or not index_flags & QtCore.Qt.ItemIsUserCheckable
):
return
if state == QtCore.Qt.Unchecked:
new_state = QtCore.Qt.Checked
else:
new_state = QtCore.Qt.Unchecked
elif event.type() == QtCore.QEvent.KeyPress:
# TODO: handle QtCore.Qt.Key_Enter, Key_Return?
if event.key() == QtCore.Qt.Key_Space:
# toogle the current items check state
if (
index_flags & QtCore.Qt.ItemIsUserCheckable
and index_flags & QtCore.Qt.ItemIsTristate
):
new_state = QtCore.Qt.CheckState((int(state) + 1) % 3)
elif index_flags & QtCore.Qt.ItemIsUserCheckable:
if state != QtCore.Qt.Checked:
new_state = QtCore.Qt.Checked
else:
new_state = QtCore.Qt.Unchecked
if new_state is not None:
model.setData(current_index, new_state, QtCore.Qt.CheckStateRole)
self.view().update(current_index)
self.value_changed.emit()
return True
def eventFilter(self, obj, event):
"""Reimplemented."""
result = self._event_popup_shown(obj, event)
if result is not None:
return result
return super(MultiSelectionComboBox, self).eventFilter(obj, event)
def addItem(self, *args, **kwargs):
idx = self.count()
super(MultiSelectionComboBox, self).addItem(*args, **kwargs)
self.model().item(idx).setCheckable(True)
def paintEvent(self, event):
"""Reimplemented."""
painter = QtWidgets.QStylePainter(self)
option = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(option)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)
# draw the icon and text
items = self.checked_items_text()
if not items:
return
text_rect = self.style().subControlRect(
QtWidgets.QStyle.CC_ComboBox,
option,
QtWidgets.QStyle.SC_ComboBoxEditField
)
text = ", ".join(items)
new_text = self.fontMetrics().elidedText(
text, QtCore.Qt.ElideRight, text_rect.width()
)
painter.drawText(
text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
new_text
)
def setItemCheckState(self, index, state):
self.setItemData(index, state, QtCore.Qt.CheckStateRole)
def set_value(self, values):
for idx in range(self.count()):
value = self.itemData(idx, role=QtCore.Qt.UserRole)
if value in values:
check_state = QtCore.Qt.Checked
else:
check_state = QtCore.Qt.Unchecked
self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole)
def value(self):
items = list()
for idx in range(self.count()):
state = self.itemData(idx, role=QtCore.Qt.CheckStateRole)
if state == QtCore.Qt.Checked:
items.append(
self.itemData(idx, role=QtCore.Qt.UserRole)
)
return items
def checked_items_text(self):
items = list()
for idx in range(self.count()):
state = self.itemData(idx, role=QtCore.Qt.CheckStateRole)
if state == QtCore.Qt.Checked:
items.append(self.itemText(idx))
return items
def wheelEvent(self, event):
event.ignore()
def keyPressEvent(self, event):
if (
event.key() == QtCore.Qt.Key_Down
and event.modifiers() & QtCore.Qt.AltModifier
):
return self.showPopup()
if event.key() in self.ignored_keys:
return event.ignore()
return super(MultiSelectionComboBox, self).keyPressEvent(event)

View file

@ -0,0 +1,98 @@
import os
from openpype import resources
from avalon.vendor import qtawesome
class ResourceCache:
colors = {
"standard": "#333333",
"new": "#2d9a4c",
"warning": "#c83232"
}
icons = None
@classmethod
def get_icon(cls, *keys):
output = cls.get_icons()
for key in keys:
output = output[key]
return output
@classmethod
def get_icons(cls):
if cls.icons is None:
cls.icons = {
"asset": {
"default": qtawesome.icon(
"fa.folder",
color=cls.colors["standard"]
),
"new": qtawesome.icon(
"fa.folder",
color=cls.colors["new"]
),
"invalid": qtawesome.icon(
"fa.exclamation-triangle",
color=cls.colors["warning"]
),
"removed": qtawesome.icon(
"fa.trash",
color=cls.colors["warning"]
)
},
"task": {
"default": qtawesome.icon(
"fa.check-circle-o",
color=cls.colors["standard"]
),
"new": qtawesome.icon(
"fa.check-circle",
color=cls.colors["new"]
),
"invalid": qtawesome.icon(
"fa.exclamation-circle",
color=cls.colors["warning"]
),
"removed": qtawesome.icon(
"fa.trash",
color=cls.colors["warning"]
)
},
"refresh": qtawesome.icon(
"fa.refresh",
color=cls.colors["standard"]
)
}
return cls.icons
@classmethod
def get_color(cls, color_name):
return cls.colors[color_name]
@classmethod
def style_fill_data(cls):
output = {}
for color_name, color_value in cls.colors.items():
key = "color:{}".format(color_name)
output[key] = color_value
return output
def load_stylesheet():
from . import qrc_resources
qrc_resources.qInitResources()
current_dir = os.path.dirname(os.path.abspath(__file__))
style_path = os.path.join(current_dir, "style.css")
with open(style_path, "r") as style_file:
stylesheet = style_file.read()
for key, value in ResourceCache.style_fill_data().items():
replacement_key = "{" + key + "}"
stylesheet = stylesheet.replace(replacement_key, value)
return stylesheet
def app_icon_path():
return resources.pype_icon_filepath()

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View file

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore
qt_resource_data = b"\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\
\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\
\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
"
qt_resource_name = b"\
\x00\x08\
\x06\xc5\x8e\xa5\
\x00\x6f\
\x00\x70\x00\x65\x00\x6e\x00\x70\x00\x79\x00\x70\x00\x65\
\x00\x06\
\x07\x03\x7d\xc3\
\x00\x69\
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
\x00\x12\
\x01\x2e\x03\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x1b\
\x03\x5a\x32\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\
\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
"
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
"
qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
if qt_version < [5, 8, 0]:
rcc_version = 1
qt_resource_struct = qt_resource_struct_v1
else:
rcc_version = 2
qt_resource_struct = qt_resource_struct_v2
def qInitResources():
QtCore.qRegisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)

View file

@ -0,0 +1,84 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 5.15.2
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore
qt_resource_data = b"\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
"
qt_resource_name = b"\
\x00\x08\
\x06\xc5\x8e\xa5\
\x00o\
\x00p\x00e\x00n\x00p\x00y\x00p\x00e\
\x00\x06\
\x07\x03}\xc3\
\x00i\
\x00m\x00a\x00g\x00e\x00s\
\x00\x12\
\x01.\x03'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\
\x00g\
\x00\x1b\
\x03Z2'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\
\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
\x00\x00\x01vA\x9d\xa25\
"
def qInitResources():
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)

View file

@ -0,0 +1,32 @@
import Qt
initialized = False
resources = None
if Qt.__binding__ == "PySide2":
from . import pyside2_resources as resources
elif Qt.__binding__ == "PyQt5":
from . import pyqt5_resources as resources
def qInitResources():
global resources
global initialized
if resources is not None and not initialized:
initialized = True
resources.qInitResources()
def qCleanupResources():
global resources
global initialized
if resources is not None:
initialized = False
resources.qCleanupResources()
__all__ = (
"resources",
"qInitResources",
"qCleanupResources"
)

View file

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/openpype">
<file>images/combobox_arrow.png</file>
<file>images/combobox_arrow_disabled.png</file>
</qresource>
</RCC>

View file

@ -0,0 +1,21 @@
QTreeView::item {
padding-top: 3px;
padding-bottom: 3px;
padding-right: 3px;
}
QTreeView::item:selected, QTreeView::item:selected:!active {
background: rgba(0, 122, 204, 127);
color: black;
}
#RefreshBtn {
padding: 2px;
}
#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {
background: transparent;
border: 1px solid #005c99;
border-radius: 0.3em;
}

View file

@ -0,0 +1,643 @@
import collections
from queue import Queue
from Qt import QtWidgets, QtCore, QtGui
from .delegates import (
NumberDelegate,
NameDelegate,
TypeDelegate,
ToolsDelegate
)
from openpype.lib import ApplicationManager
from .constants import (
REMOVED_ROLE,
IDENTIFIER_ROLE,
ITEM_TYPE_ROLE,
HIERARCHY_CHANGE_ABLE_ROLE,
EDITOR_OPENED_ROLE
)
class NameDef:
pass
class NumberDef:
def __init__(self, minimum=None, maximum=None, decimals=None):
self.minimum = 0 if minimum is None else minimum
self.maximum = 999999 if maximum is None else maximum
self.decimals = 0 if decimals is None else decimals
class TypeDef:
pass
class ToolsDef:
pass
class ProjectDocCache:
def __init__(self, dbcon):
self.dbcon = dbcon
self.project_doc = None
def set_project(self, project_name):
self.project_doc = None
if not project_name:
return
self.project_doc = self.dbcon.database[project_name].find_one(
{"type": "project"}
)
class ToolsCache:
def __init__(self):
self.tools_data = []
def refresh(self):
app_manager = ApplicationManager()
tools_data = []
for tool_name, tool in app_manager.tools.items():
tools_data.append(
(tool_name, tool.label)
)
self.tools_data = tools_data
class HierarchyView(QtWidgets.QTreeView):
"""A tree view that deselects on clicking on an empty area in the view"""
column_delegate_defs = {
"name": NameDef(),
"type": TypeDef(),
"frameStart": NumberDef(1),
"frameEnd": NumberDef(1),
"fps": NumberDef(1, decimals=2),
"resolutionWidth": NumberDef(0),
"resolutionHeight": NumberDef(0),
"handleStart": NumberDef(0),
"handleEnd": NumberDef(0),
"clipIn": NumberDef(1),
"clipOut": NumberDef(1),
"pixelAspect": NumberDef(0, decimals=2),
"tools_env": ToolsDef()
}
columns_sizes = {
"default": {
"stretch": QtWidgets.QHeaderView.ResizeToContents
},
"name": {
"stretch": QtWidgets.QHeaderView.Stretch
},
"type": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 100
},
"tools_env": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 140
},
"pixelAspect": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 80
}
}
persistent_columns = {
"type",
"frameStart",
"frameEnd",
"fps",
"resolutionWidth",
"resolutionHeight",
"handleStart",
"handleEnd",
"clipIn",
"clipOut",
"pixelAspect",
"tools_env"
}
def __init__(self, dbcon, source_model, parent):
super(HierarchyView, self).__init__(parent)
# Direct access to model
self._source_model = source_model
self._editors_mapping = {}
self._persisten_editors = set()
# Access to parent because of `show_message` method
self._parent = parent
project_doc_cache = ProjectDocCache(dbcon)
tools_cache = ToolsCache()
main_delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(main_delegate)
self.setAlternatingRowColors(True)
self.setSelectionMode(HierarchyView.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
column_delegates = {}
column_key_to_index = {}
for key, item_type in self.column_delegate_defs.items():
if isinstance(item_type, NameDef):
delegate = NameDelegate()
elif isinstance(item_type, NumberDef):
delegate = NumberDelegate(
item_type.minimum,
item_type.maximum,
item_type.decimals
)
elif isinstance(item_type, TypeDef):
delegate = TypeDelegate(project_doc_cache)
elif isinstance(item_type, ToolsDef):
delegate = ToolsDelegate(tools_cache)
column = self._source_model.columns.index(key)
self.setItemDelegateForColumn(column, delegate)
column_delegates[key] = delegate
column_key_to_index[key] = column
source_model.index_moved.connect(self._on_rows_moved)
self.customContextMenuRequested.connect(self._on_context_menu)
self._source_model.project_changed.connect(self._on_project_reset)
self._project_doc_cache = project_doc_cache
self._tools_cache = tools_cache
self._delegate = main_delegate
self._column_delegates = column_delegates
self._column_key_to_index = column_key_to_index
def header_init(self):
header = self.header()
header.setStretchLastSection(False)
default_behavior = self.columns_sizes["default"]
widths_by_idx = {}
for idx in range(header.count()):
key = self._source_model.columns[idx]
behavior = self.columns_sizes.get(key, default_behavior)
logical_index = header.logicalIndex(idx)
stretch = behavior["stretch"]
header.setSectionResizeMode(logical_index, stretch)
width = behavior.get("width")
if width is not None:
widths_by_idx[idx] = width
for idx, width in widths_by_idx.items():
self.setColumnWidth(idx, width)
def set_project(self, project_name):
# Trigger helpers first
self._project_doc_cache.set_project(project_name)
self._tools_cache.refresh()
# Trigger update of model after all data for delegates are filled
self._source_model.set_project(project_name)
def _on_project_reset(self):
self.header_init()
self.collapseAll()
project_item = self._source_model.project_item
if project_item:
index = self._source_model.index_for_item(project_item)
self.expand(index)
def _on_rows_moved(self, index):
parent_index = index.parent()
if not self.isExpanded(parent_index):
self.expand(parent_index)
def commitData(self, editor):
super(HierarchyView, self).commitData(editor)
current_index = self.currentIndex()
column = current_index.column()
row = current_index.row()
skipped_index = None
# Change column from "type" to "name"
if column == 1:
new_index = self._source_model.index(
current_index.row(),
0,
current_index.parent()
)
self.setCurrentIndex(new_index)
elif column > 0:
indexes = []
for index in self.selectedIndexes():
if index.column() == column:
if index.row() == row:
skipped_index = index
else:
indexes.append(index)
if skipped_index is not None:
value = current_index.data(QtCore.Qt.EditRole)
for index in indexes:
index.model().setData(index, value, QtCore.Qt.EditRole)
# Update children data
self.updateEditorData()
def _deselect_editor(self, editor):
if editor:
if isinstance(
editor, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)
):
line_edit = editor.findChild(QtWidgets.QLineEdit)
line_edit.deselect()
elif isinstance(editor, QtWidgets.QLineEdit):
editor.deselect()
def edit(self, index, *args, **kwargs):
result = super(HierarchyView, self).edit(index, *args, **kwargs)
if result:
# Mark index to not return text for DisplayRole
editor = self.indexWidget(index)
if (
editor not in self._persisten_editors
and editor not in self._editors_mapping
):
self._editors_mapping[editor] = index
self._source_model.setData(index, True, EDITOR_OPENED_ROLE)
# Deselect content of editor
# QUESTION not sure if we want do this all the time
self._deselect_editor(editor)
return result
def closeEditor(self, editor, hint):
if (
editor not in self._persisten_editors
and editor in self._editors_mapping
):
index = self._editors_mapping.pop(editor)
self._source_model.setData(index, False, EDITOR_OPENED_ROLE)
super(HierarchyView, self).closeEditor(editor, hint)
def openPersistentEditor(self, index):
self._source_model.setData(index, True, EDITOR_OPENED_ROLE)
super(HierarchyView, self).openPersistentEditor(index)
editor = self.indexWidget(index)
self._persisten_editors.add(editor)
self._deselect_editor(editor)
def closePersistentEditor(self, index):
self._source_model.setData(index, False, EDITOR_OPENED_ROLE)
editor = self.indexWidget(index)
self._persisten_editors.remove(editor)
super(HierarchyView, self).closePersistentEditor(index)
def rowsInserted(self, parent_index, start, end):
super(HierarchyView, self).rowsInserted(parent_index, start, end)
for row in range(start, end + 1):
for key, column in self._column_key_to_index.items():
if key not in self.persistent_columns:
continue
col_index = self._source_model.index(row, column, parent_index)
if bool(
self._source_model.flags(col_index)
& QtCore.Qt.ItemIsEditable
):
self.openPersistentEditor(col_index)
# Expand parent on insert
if not self.isExpanded(parent_index):
self.expand(parent_index)
def mousePressEvent(self, event):
index = self.indexAt(event.pos())
if not index.isValid():
# clear the selection
self.clearSelection()
# clear the current index
self.setCurrentIndex(QtCore.QModelIndex())
super(HierarchyView, self).mousePressEvent(event)
def keyPressEvent(self, event):
call_super = False
if event.key() == QtCore.Qt.Key_Delete:
self._delete_items()
elif event.matches(QtGui.QKeySequence.Copy):
self._copy_items()
elif event.matches(QtGui.QKeySequence.Paste):
self._paste_items()
elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
mdfs = event.modifiers()
if mdfs == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier):
self._on_ctrl_shift_enter_pressed()
elif mdfs == QtCore.Qt.ShiftModifier:
self._on_shift_enter_pressed()
else:
if self.state() == HierarchyView.NoState:
self._on_enter_pressed()
elif event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() == QtCore.Qt.Key_Left:
self._on_left_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Right:
self._on_right_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Up:
self._on_up_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Down:
self._on_down_ctrl_pressed()
else:
call_super = True
if call_super:
super(HierarchyView, self).keyPressEvent(event)
else:
event.accept()
def _copy_items(self, indexes=None):
try:
if indexes is None:
indexes = self.selectedIndexes()
mime_data = self._source_model.copy_mime_data(indexes)
QtWidgets.QApplication.clipboard().setMimeData(mime_data)
self._show_message("Tasks copied")
except ValueError as exc:
self._show_message(str(exc))
def _paste_items(self):
index = self.currentIndex()
mime_data = QtWidgets.QApplication.clipboard().mimeData()
self._source_model.paste_mime_data(index, mime_data)
def _delete_items(self, indexes=None):
if indexes is None:
indexes = self.selectedIndexes()
self._source_model.delete_indexes(indexes)
def _on_ctrl_shift_enter_pressed(self):
self._add_task_and_edit()
def add_asset(self, parent_index=None):
if parent_index is None:
parent_index = self.currentIndex()
if not parent_index.isValid():
return
# Stop editing
self.setState(HierarchyView.NoState)
QtWidgets.QApplication.processEvents()
return self._source_model.add_new_asset(parent_index)
def add_task(self, parent_index=None):
if parent_index is None:
parent_index = self.currentIndex()
if not parent_index.isValid():
return
return self._source_model.add_new_task(parent_index)
def _add_asset_and_edit(self, parent_index=None):
new_index = self.add_asset(parent_index)
if new_index is None:
return
# Change current index
self.selectionModel().setCurrentIndex(
new_index,
QtCore.QItemSelectionModel.Clear
| QtCore.QItemSelectionModel.Select
)
# Start editing
self.edit(new_index)
def _add_task_and_edit(self):
new_index = self.add_task()
if new_index is None:
return
# Stop editing
self.setState(HierarchyView.NoState)
QtWidgets.QApplication.processEvents()
# TODO change hardcoded column index to coded
task_type_index = self._source_model.index(
new_index.row(), 1, new_index.parent()
)
# Change current index
self.selectionModel().setCurrentIndex(
task_type_index,
QtCore.QItemSelectionModel.Clear
| QtCore.QItemSelectionModel.Select
)
# Start editing
self.edit(task_type_index)
def _on_shift_enter_pressed(self):
parent_index = self.currentIndex()
if not parent_index.isValid():
return
if parent_index.data(ITEM_TYPE_ROLE) == "asset":
parent_index = parent_index.parent()
self._add_asset_and_edit(parent_index)
def _on_up_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_vertical(indexes, -1)
def _on_down_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_vertical(indexes, 1)
def _on_left_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_horizontal(indexes, -1)
def _on_right_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_horizontal(indexes, 1)
def _on_enter_pressed(self):
index = self.currentIndex()
if (
index.isValid()
and index.flags() & QtCore.Qt.ItemIsEditable
):
self.edit(index)
def _remove_delete_flag(self, item_ids):
"""Remove deletion flag on items marked for deletion."""
self._source_model.remove_delete_flag(item_ids)
def _expand_items(self, indexes):
"""Expand multiple items with all it's children.
Args:
indexes (list): List of QModelIndex that should be expanded.
"""
process_queue = Queue()
for index in indexes:
if index.column() == 0:
process_queue.put(index)
item_ids = set()
# Use deque as expanding not visible items as first is faster
indexes_deque = collections.deque()
while not process_queue.empty():
index = process_queue.get()
item_id = index.data(IDENTIFIER_ROLE)
if item_id in item_ids:
continue
item_ids.add(item_id)
indexes_deque.append(index)
for row in range(self._source_model.rowCount(index)):
process_queue.put(self._source_model.index(
row, 0, index
))
while indexes_deque:
self.expand(indexes_deque.pop())
def _collapse_items(self, indexes):
"""Collapse multiple items with all it's children.
Args:
indexes (list): List of QModelIndex that should be collapsed.
"""
item_ids = set()
process_queue = Queue()
for index in indexes:
if index.column() == 0:
process_queue.put(index)
while not process_queue.empty():
index = process_queue.get()
item_id = index.data(IDENTIFIER_ROLE)
if item_id in item_ids:
continue
item_ids.add(item_id)
self.collapse(index)
for row in range(self._source_model.rowCount(index)):
process_queue.put(self._source_model.index(
row, 0, index
))
def _show_message(self, message):
"""Show message to user."""
self._parent.show_message(message)
def _on_context_menu(self, point):
"""Context menu on right click.
Currently is menu shown only on "name" column.
"""
index = self.indexAt(point)
column = index.column()
if column != 0:
return
actions = []
context_menu = QtWidgets.QMenu(self)
indexes = self.selectedIndexes()
items_by_id = {}
for index in indexes:
if index.column() != column:
continue
item_id = index.data(IDENTIFIER_ROLE)
items_by_id[item_id] = self._source_model.items_by_id[item_id]
item_ids = tuple(items_by_id.keys())
if len(item_ids) == 1:
item = items_by_id[item_ids[0]]
item_type = item.data(ITEM_TYPE_ROLE)
if item_type in ("asset", "project"):
add_asset_action = QtWidgets.QAction("Add Asset", context_menu)
add_asset_action.triggered.connect(
self._add_asset_and_edit
)
actions.append(add_asset_action)
if item_type in ("asset", "task"):
add_task_action = QtWidgets.QAction("Add Task", context_menu)
add_task_action.triggered.connect(
self._add_task_and_edit
)
actions.append(add_task_action)
# Remove delete tag on items
removed_item_ids = []
show_delete_items = False
for item_id, item in items_by_id.items():
if item.data(REMOVED_ROLE):
removed_item_ids.append(item_id)
elif (
not show_delete_items
and item.data(ITEM_TYPE_ROLE) != "project"
and item.data(HIERARCHY_CHANGE_ABLE_ROLE)
):
show_delete_items = True
if show_delete_items:
action = QtWidgets.QAction("Delete items", context_menu)
action.triggered.connect(
lambda: self._delete_items()
)
actions.append(action)
if removed_item_ids:
action = QtWidgets.QAction("Keep items", context_menu)
action.triggered.connect(
lambda: self._remove_delete_flag(removed_item_ids)
)
actions.append(action)
# Collapse/Expand action
show_collapse_expand_action = False
for item_id in item_ids:
item = items_by_id[item_id]
item_type = item.data(ITEM_TYPE_ROLE)
if item_type != "task":
show_collapse_expand_action = True
break
if show_collapse_expand_action:
expand_action = QtWidgets.QAction("Expand all", context_menu)
collapse_action = QtWidgets.QAction("Collapse all", context_menu)
expand_action.triggered.connect(
lambda: self._expand_items(indexes)
)
collapse_action.triggered.connect(
lambda: self._collapse_items(indexes)
)
actions.append(expand_action)
actions.append(collapse_action)
if not actions:
return
for action in actions:
context_menu.addAction(action)
global_point = self.viewport().mapToGlobal(point)
context_menu.exec_(global_point)

View file

@ -0,0 +1,281 @@
import re
from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
PROJECT_NAME_REGEX
)
from avalon.api import AvalonMongoDB
from Qt import QtWidgets, QtCore
class NameTextEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super(NameTextEdit, self).__init__(*args, **kwargs)
self.textChanged.connect(self._on_text_change)
def _on_text_change(self, text):
if NAME_REGEX.match(text):
return
idx = self.cursorPosition()
before_text = text[0:idx]
after_text = text[idx:len(text)]
sub_regex = "[^{}]+".format(NAME_ALLOWED_SYMBOLS)
new_before_text = re.sub(sub_regex, "", before_text)
new_after_text = re.sub(sub_regex, "", after_text)
idx -= (len(before_text) - len(new_before_text))
self.setText(new_before_text + new_after_text)
self.setCursorPosition(idx)
class FilterComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(FilterComboBox, self).__init__(parent)
self._last_value = None
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setEditable(True)
filter_proxy_model = QtCore.QSortFilterProxyModel(self)
filter_proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
filter_proxy_model.setSourceModel(self.model())
completer = QtWidgets.QCompleter(filter_proxy_model, self)
completer.setCompletionMode(
QtWidgets.QCompleter.UnfilteredPopupCompletion
)
self.setCompleter(completer)
self.lineEdit().textEdited.connect(
filter_proxy_model.setFilterFixedString
)
completer.activated.connect(self.on_completer_activated)
self._completer = completer
self._filter_proxy_model = filter_proxy_model
def focusInEvent(self, event):
super(FilterComboBox, self).focusInEvent(event)
self._last_value = self.lineEdit().text()
self.lineEdit().selectAll()
def value_cleanup(self):
text = self.lineEdit().text()
idx = self.findText(text)
if idx < 0:
count = self._completer.completionModel().rowCount()
if count > 0:
index = self._completer.completionModel().index(0, 0)
text = index.data(QtCore.Qt.DisplayRole)
idx = self.findText(text)
elif self._last_value is not None:
idx = self.findText(self._last_value)
if idx < 0:
idx = 0
self.setCurrentIndex(idx)
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self.value_cleanup()
super(FilterComboBox, self).keyPressEvent(event)
def setModel(self, model):
super(FilterComboBox, self).setModel(model)
self._filter_proxy_model.setSourceModel(model)
self._completer.setModel(self._filter_proxy_model)
def setModelColumn(self, column):
self._completer.setCompletionColumn(column)
self._filter_proxy_model.setFilterKeyColumn(column)
super(FilterComboBox, self).setModelColumn(column)
class CreateProjectDialog(QtWidgets.QDialog):
def __init__(self, parent=None, dbcon=None):
super(CreateProjectDialog, self).__init__(parent)
self.setWindowTitle("Create Project")
self.allowed_regex = "[^{}]+".format(PROJECT_NAME_ALLOWED_SYMBOLS)
if dbcon is None:
dbcon = AvalonMongoDB()
self.dbcon = dbcon
self._ignore_code_change = False
self._project_name_is_valid = False
self._project_code_is_valid = False
self._project_code_value = None
project_names, project_codes = self._get_existing_projects()
inputs_widget = QtWidgets.QWidget(self)
project_name_input = QtWidgets.QLineEdit(inputs_widget)
project_code_input = QtWidgets.QLineEdit(inputs_widget)
library_project_input = QtWidgets.QCheckBox(inputs_widget)
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
inputs_layout.setContentsMargins(0, 0, 0, 0)
inputs_layout.addRow("Project name:", project_name_input)
inputs_layout.addRow("Project code:", project_code_input)
inputs_layout.addRow("Library project:", library_project_input)
project_name_label = QtWidgets.QLabel(self)
project_code_label = QtWidgets.QLabel(self)
btns_widget = QtWidgets.QWidget(self)
ok_btn = QtWidgets.QPushButton("Ok", btns_widget)
ok_btn.setEnabled(False)
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn)
btns_layout.addWidget(cancel_btn)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(inputs_widget, 0)
main_layout.addWidget(project_name_label, 1)
main_layout.addWidget(project_code_label, 1)
main_layout.addStretch(1)
main_layout.addWidget(btns_widget, 0)
project_name_input.textChanged.connect(self._on_project_name_change)
project_code_input.textChanged.connect(self._on_project_code_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self.invalid_project_names = project_names
self.invalid_project_codes = project_codes
self.project_name_label = project_name_label
self.project_code_label = project_code_label
self.project_name_input = project_name_input
self.project_code_input = project_code_input
self.library_project_input = library_project_input
self.ok_btn = ok_btn
@property
def project_name(self):
return self.project_name_input.text()
def _on_project_name_change(self, value):
if self._project_code_value is None:
self._ignore_code_change = True
self.project_code_input.setText(value.lower())
self._ignore_code_change = False
self._update_valid_project_name(value)
def _on_project_code_change(self, value):
if not value:
value = None
self._update_valid_project_code(value)
if not self._ignore_code_change:
self._project_code_value = value
def _update_valid_project_name(self, value):
message = ""
is_valid = True
if not value:
message = "Project name is empty"
is_valid = False
elif value in self.invalid_project_names:
message = "Project name \"{}\" already exist".format(value)
is_valid = False
elif not PROJECT_NAME_REGEX.match(value):
message = (
"Project name \"{}\" contain not supported symbols"
).format(value)
is_valid = False
self._project_name_is_valid = is_valid
self.project_name_label.setText(message)
self.project_name_label.setVisible(bool(message))
self._enable_button()
def _update_valid_project_code(self, value):
message = ""
is_valid = True
if not value:
message = "Project code is empty"
is_valid = False
elif value in self.invalid_project_names:
message = "Project code \"{}\" already exist".format(value)
is_valid = False
elif not PROJECT_NAME_REGEX.match(value):
message = (
"Project code \"{}\" contain not supported symbols"
).format(value)
is_valid = False
self._project_code_is_valid = is_valid
self.project_code_label.setText(message)
self._enable_button()
def _enable_button(self):
self.ok_btn.setEnabled(
self._project_name_is_valid and self._project_code_is_valid
)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
if not self._project_name_is_valid or not self._project_code_is_valid:
return
project_name = self.project_name_input.text()
project_code = self.project_code_input.text()
library_project = self.library_project_input.isChecked()
create_project(project_name, project_code, library_project, self.dbcon)
self.done(1)
def _get_existing_projects(self):
project_names = set()
project_codes = set()
for project_name in self.dbcon.database.collection_names():
# Each collection will have exactly one project document
project_doc = self.dbcon.database[project_name].find_one(
{"type": "project"},
{"name": 1, "data.code": 1}
)
if not project_doc:
continue
project_name = project_doc.get("name")
if not project_name:
continue
project_names.add(project_name)
project_code = project_doc.get("data", {}).get("code")
if not project_code:
project_code = project_name
project_codes.add(project_code)
return project_names, project_codes

View file

@ -0,0 +1,176 @@
from Qt import QtWidgets, QtCore, QtGui
from . import (
ProjectModel,
HierarchyModel,
HierarchySelectionModel,
HierarchyView,
CreateProjectDialog
)
from .style import load_stylesheet, ResourceCache
from openpype import resources
from avalon.api import AvalonMongoDB
class ProjectManagerWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ProjectManagerWindow, self).__init__(parent)
self.setWindowTitle("OpenPype Project Manager")
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
# Top part of window
top_part_widget = QtWidgets.QWidget(self)
# Project selection
project_widget = QtWidgets.QWidget(top_part_widget)
dbcon = AvalonMongoDB()
project_model = ProjectModel(dbcon)
project_combobox = QtWidgets.QComboBox(project_widget)
project_combobox.setModel(project_model)
project_combobox.setRootModelIndex(QtCore.QModelIndex())
refresh_projects_btn = QtWidgets.QPushButton(project_widget)
refresh_projects_btn.setIcon(ResourceCache.get_icon("refresh"))
refresh_projects_btn.setToolTip("Refresh projects")
refresh_projects_btn.setObjectName("RefreshBtn")
create_project_btn = QtWidgets.QPushButton(
"Create project...", project_widget
)
project_layout = QtWidgets.QHBoxLayout(project_widget)
project_layout.setContentsMargins(0, 0, 0, 0)
project_layout.addWidget(project_combobox, 0)
project_layout.addWidget(refresh_projects_btn, 0)
project_layout.addWidget(create_project_btn, 0)
project_layout.addStretch(1)
# Helper buttons
helper_btns_widget = QtWidgets.QWidget(top_part_widget)
helper_label = QtWidgets.QLabel("Add:", helper_btns_widget)
add_asset_btn = QtWidgets.QPushButton(
ResourceCache.get_icon("asset", "default"),
"Asset",
helper_btns_widget
)
add_task_btn = QtWidgets.QPushButton(
ResourceCache.get_icon("task", "default"),
"Task",
helper_btns_widget
)
helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget)
helper_btns_layout.setContentsMargins(0, 0, 0, 0)
helper_btns_layout.addWidget(helper_label)
helper_btns_layout.addWidget(add_asset_btn)
helper_btns_layout.addWidget(add_task_btn)
helper_btns_layout.addStretch(1)
# Add widgets to top widget layout
top_part_layout = QtWidgets.QVBoxLayout(top_part_widget)
top_part_layout.setContentsMargins(0, 0, 0, 0)
top_part_layout.addWidget(project_widget)
top_part_layout.addWidget(helper_btns_widget)
hierarchy_model = HierarchyModel(dbcon)
hierarchy_view = HierarchyView(dbcon, hierarchy_model, self)
hierarchy_view.setModel(hierarchy_model)
_selection_model = HierarchySelectionModel(
hierarchy_model.multiselection_column_indexes
)
_selection_model.setModel(hierarchy_view.model())
hierarchy_view.setSelectionModel(_selection_model)
buttons_widget = QtWidgets.QWidget(self)
message_label = QtWidgets.QLabel(buttons_widget)
save_btn = QtWidgets.QPushButton("Save", buttons_widget)
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.addWidget(message_label)
buttons_layout.addStretch(1)
buttons_layout.addWidget(save_btn)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(top_part_widget)
main_layout.addWidget(hierarchy_view)
main_layout.addWidget(buttons_widget)
refresh_projects_btn.clicked.connect(self._on_project_refresh)
create_project_btn.clicked.connect(self._on_project_create)
project_combobox.currentIndexChanged.connect(self._on_project_change)
save_btn.clicked.connect(self._on_save_click)
add_asset_btn.clicked.connect(self._on_add_asset)
add_task_btn.clicked.connect(self._on_add_task)
self.project_model = project_model
self.project_combobox = project_combobox
self.hierarchy_view = hierarchy_view
self.hierarchy_model = hierarchy_model
self.message_label = message_label
self.resize(1200, 600)
self.setStyleSheet(load_stylesheet())
self.refresh_projects()
def _set_project(self, project_name=None):
self.hierarchy_view.set_project(project_name)
def refresh_projects(self, project_name=None):
if project_name is None:
if self.project_combobox.count() > 0:
project_name = self.project_combobox.currentText()
self.project_model.refresh()
if self.project_combobox.count() == 0:
return self._set_project()
if project_name:
row = self.project_combobox.findText(project_name)
if row >= 0:
self.project_combobox.setCurrentIndex(row)
self._set_project(self.project_combobox.currentText())
def _on_project_change(self):
self._set_project(self.project_combobox.currentText())
def _on_project_refresh(self):
self.refresh_projects()
def _on_save_click(self):
self.hierarchy_model.save()
def _on_add_asset(self):
self.hierarchy_view.add_asset()
def _on_add_task(self):
self.hierarchy_view.add_task()
def show_message(self, message):
# TODO add nicer message pop
self.message_label.setText(message)
def _on_project_create(self):
dialog = CreateProjectDialog(self)
dialog.exec_()
if dialog.result() != 1:
return
project_name = dialog.project_name
self.show_message("Created project \"{}\"".format(project_name))
self.refresh_projects(project_name)

View file

@ -0,0 +1,18 @@
<#
.SYNOPSIS
Helper script OpenPype Tray.
.DESCRIPTION
.EXAMPLE
PS> .\run_tray.ps1
#>
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
Set-Location -Path $openpype_root
& poetry run python "$($openpype_root)\start.py" projectmanager
Set-Location -Path $current_dir