mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1396 from pypeclub/feature/project_manager
This commit is contained in:
commit
2b8233de53
25 changed files with 4038 additions and 8 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
10
openpype/tools/project_manager/__init__.py
Normal file
10
openpype/tools/project_manager/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .project_manager import (
|
||||
ProjectManagerWindow,
|
||||
main
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ProjectManagerWindow",
|
||||
"main"
|
||||
)
|
||||
5
openpype/tools/project_manager/__main__.py
Normal file
5
openpype/tools/project_manager/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from project_manager import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
openpype/tools/project_manager/project_manager/__init__.py
Normal file
50
openpype/tools/project_manager/project_manager/__init__.py
Normal 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_())
|
||||
13
openpype/tools/project_manager/project_manager/constants.py
Normal file
13
openpype/tools/project_manager/project_manager/constants.py
Normal 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 + "]*$")
|
||||
159
openpype/tools/project_manager/project_manager/delegates.py
Normal file
159
openpype/tools/project_manager/project_manager/delegates.py
Normal 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)
|
||||
2004
openpype/tools/project_manager/project_manager/model.py
Normal file
2004
openpype/tools/project_manager/project_manager/model.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
@ -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 |
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<RCC>
|
||||
<qresource prefix="/openpype">
|
||||
<file>images/combobox_arrow.png</file>
|
||||
<file>images/combobox_arrow_disabled.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
@ -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;
|
||||
}
|
||||
643
openpype/tools/project_manager/project_manager/view.py
Normal file
643
openpype/tools/project_manager/project_manager/view.py
Normal 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)
|
||||
281
openpype/tools/project_manager/project_manager/widgets.py
Normal file
281
openpype/tools/project_manager/project_manager/widgets.py
Normal 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
|
||||
176
openpype/tools/project_manager/project_manager/window.py
Normal file
176
openpype/tools/project_manager/project_manager/window.py
Normal 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)
|
||||
18
tools/run_project_manager.ps1
Normal file
18
tools/run_project_manager.ps1
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue