mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
203c716db8
33 changed files with 6432 additions and 28 deletions
4
.github/workflows/prerelease.yml
vendored
4
.github/workflows/prerelease.yml
vendored
|
|
@ -20,12 +20,12 @@ jobs:
|
|||
python-version: 3.7
|
||||
|
||||
- name: Install Python requirements
|
||||
run: pip install gitpython semver
|
||||
run: pip install gitpython semver PyGithub
|
||||
|
||||
- name: 🔎 Determine next version type
|
||||
id: version_type
|
||||
run: |
|
||||
TYPE=$(python ./tools/ci_tools.py --bump)
|
||||
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }})
|
||||
|
||||
echo ::set-output name=type::$TYPE
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ from .pipeline import (
|
|||
|
||||
from avalon.tools import (
|
||||
creator,
|
||||
loader,
|
||||
sceneinventory,
|
||||
)
|
||||
from openpype.tools import (
|
||||
loader,
|
||||
libraryloader
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Basic avalon integration
|
|||
"""
|
||||
import os
|
||||
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from avalon import api as avalon
|
||||
from pyblish import api as pyblish
|
||||
from openpype.api import Logger
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ def menu_install():
|
|||
apply_colorspace_project, apply_colorspace_clips
|
||||
)
|
||||
# here is the best place to add menu
|
||||
from avalon.tools import cbloader, creator, sceneinventory
|
||||
from avalon.tools import creator, sceneinventory
|
||||
from openpype.tools import loader
|
||||
from avalon.vendor.Qt import QtGui
|
||||
|
||||
menu_name = os.environ['AVALON_LABEL']
|
||||
|
|
@ -90,7 +91,7 @@ def menu_install():
|
|||
|
||||
loader_action = menu.addAction("Load ...")
|
||||
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
loader_action.triggered.connect(cbloader.show)
|
||||
loader_action.triggered.connect(loader.show)
|
||||
|
||||
sceneinventory_action = menu.addAction("Manage ...")
|
||||
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ Basic avalon integration
|
|||
import os
|
||||
import contextlib
|
||||
from collections import OrderedDict
|
||||
from avalon.tools import (
|
||||
workfiles,
|
||||
publish as _publish
|
||||
)
|
||||
from avalon.tools import publish as _publish
|
||||
from openpype.tools import workfiles
|
||||
from avalon.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon import api as avalon
|
||||
from avalon import schema
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ creator.show()
|
|||
<scriptItem id="avalon_load">
|
||||
<label>Load ...</label>
|
||||
<scriptCode><![CDATA[
|
||||
from avalon.tools import cbloader
|
||||
cbloader.show(use_context=True)
|
||||
from openpype.tools import loader
|
||||
loader.show(use_context=True)
|
||||
]]></scriptCode>
|
||||
</scriptItem>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from avalon import api as avalon
|
|||
from avalon import pipeline
|
||||
from avalon.maya import suspended_refresh
|
||||
from avalon.maya.pipeline import IS_HEADLESS
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from pyblish import api as pyblish
|
||||
from openpype.lib import any_outdated
|
||||
import openpype.hosts.maya
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ def override_toolbox_ui():
|
|||
log.warning("Could not import SceneInventory tool")
|
||||
|
||||
try:
|
||||
import avalon.tools.loader as loader
|
||||
import openpype.tools.loader as loader
|
||||
except Exception:
|
||||
log.warning("Could not import Loader tool")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||
|
||||
|
||||
from avalon import api, io, lib
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
import avalon.nuke
|
||||
from avalon.nuke import lib as anlib
|
||||
from avalon.nuke import (
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ from .pipeline import (
|
|||
|
||||
from avalon.tools import (
|
||||
creator,
|
||||
loader,
|
||||
sceneinventory,
|
||||
libraryloader,
|
||||
subsetmanager
|
||||
)
|
||||
from openpype.tools import (
|
||||
loader,
|
||||
libraryloader,
|
||||
)
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Basic avalon integration
|
|||
import os
|
||||
import contextlib
|
||||
from collections import OrderedDict
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from avalon import api as avalon
|
||||
from avalon import schema
|
||||
from avalon.pipeline import AVALON_CONTAINER_ID
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
|
|||
def tray_init(self):
|
||||
# Add library tool
|
||||
try:
|
||||
from avalon.tools.libraryloader import app
|
||||
from avalon import style
|
||||
from Qt import QtGui
|
||||
from avalon import style
|
||||
from openpype.tools.libraryloader import LibraryLoaderWindow
|
||||
|
||||
self.libraryloader = app.Window(
|
||||
self.libraryloader = LibraryLoaderWindow(
|
||||
icon=QtGui.QIcon(resources.get_openpype_icon_filepath()),
|
||||
show_projects=True,
|
||||
show_libraries=True
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from bson.objectid import ObjectId
|
|||
from Qt import QtCore
|
||||
from Qt.QtCore import Qt
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from openpype.tools.utils.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from openpype.tools.settings import (
|
|||
from openpype.api import get_local_site_id
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from openpype.tools.utils.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from .models import (
|
||||
|
|
|
|||
11
openpype/tools/libraryloader/__init__.py
Normal file
11
openpype/tools/libraryloader/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from .app import (
|
||||
LibraryLoaderWindow,
|
||||
show,
|
||||
cli
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LibraryLoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
]
|
||||
5
openpype/tools/libraryloader/__main__.py
Normal file
5
openpype/tools/libraryloader/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
591
openpype/tools/libraryloader/app.py
Normal file
591
openpype/tools/libraryloader/app.py
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
import sys
|
||||
import time
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import style
|
||||
from avalon.api import AvalonMongoDB
|
||||
from openpype.tools.utils import lib as tools_lib
|
||||
from openpype.tools.loader.widgets import (
|
||||
ThumbnailWidget,
|
||||
VersionWidget,
|
||||
FamilyListWidget,
|
||||
RepresentationWidget
|
||||
)
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
from . import lib
|
||||
from .widgets import LibrarySubsetWidget
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class LibraryLoaderWindow(QtWidgets.QDialog):
|
||||
"""Asset library loader interface"""
|
||||
|
||||
tool_title = "Library Loader 0.5"
|
||||
tool_name = "library_loader"
|
||||
|
||||
def __init__(
|
||||
self, parent=None, icon=None, show_projects=False, show_libraries=True
|
||||
):
|
||||
super(LibraryLoaderWindow, self).__init__(parent)
|
||||
|
||||
self._initial_refresh = False
|
||||
self._ignore_project_change = False
|
||||
|
||||
# Enable minimize and maximize for app
|
||||
self.setWindowTitle(self.tool_title)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
if icon is not None:
|
||||
self.setWindowIcon(icon)
|
||||
# self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = QtWidgets.QWidget()
|
||||
|
||||
self.dbcon = AvalonMongoDB()
|
||||
self.dbcon.install()
|
||||
self.dbcon.Session["AVALON_PROJECT"] = None
|
||||
|
||||
self.show_projects = show_projects
|
||||
self.show_libraries = show_libraries
|
||||
|
||||
# Groups config
|
||||
self.groups_config = tools_lib.GroupsConfig(self.dbcon)
|
||||
self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon)
|
||||
|
||||
assets = AssetWidget(
|
||||
self.dbcon, multiselection=True, parent=self
|
||||
)
|
||||
families = FamilyListWidget(
|
||||
self.dbcon, self.family_config_cache, parent=self
|
||||
)
|
||||
subsets = LibrarySubsetWidget(
|
||||
self.dbcon,
|
||||
self.groups_config,
|
||||
self.family_config_cache,
|
||||
tool_name=self.tool_name,
|
||||
parent=self
|
||||
)
|
||||
|
||||
version = VersionWidget(self.dbcon)
|
||||
thumbnail = ThumbnailWidget(self.dbcon)
|
||||
|
||||
# Project
|
||||
self.combo_projects = QtWidgets.QComboBox()
|
||||
|
||||
# Create splitter to show / hide family filters
|
||||
asset_filter_splitter = QtWidgets.QSplitter()
|
||||
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
asset_filter_splitter.addWidget(self.combo_projects)
|
||||
asset_filter_splitter.addWidget(assets)
|
||||
asset_filter_splitter.addWidget(families)
|
||||
asset_filter_splitter.setStretchFactor(1, 65)
|
||||
asset_filter_splitter.setStretchFactor(2, 35)
|
||||
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
|
||||
representations = RepresentationWidget(self.dbcon)
|
||||
thumb_ver_splitter = QtWidgets.QSplitter()
|
||||
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
thumb_ver_splitter.addWidget(thumbnail)
|
||||
thumb_ver_splitter.addWidget(version)
|
||||
if sync_server.enabled:
|
||||
thumb_ver_splitter.addWidget(representations)
|
||||
thumb_ver_splitter.setStretchFactor(0, 30)
|
||||
thumb_ver_splitter.setStretchFactor(1, 35)
|
||||
|
||||
container_layout = QtWidgets.QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
split = QtWidgets.QSplitter()
|
||||
split.addWidget(asset_filter_splitter)
|
||||
split.addWidget(subsets)
|
||||
split.addWidget(thumb_ver_splitter)
|
||||
split.setSizes([180, 950, 200])
|
||||
container_layout.addWidget(split)
|
||||
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.data = {
|
||||
"widgets": {
|
||||
"families": families,
|
||||
"assets": assets,
|
||||
"subsets": subsets,
|
||||
"version": version,
|
||||
"thumbnail": thumbnail,
|
||||
"representations": representations
|
||||
},
|
||||
"label": {
|
||||
"message": message,
|
||||
},
|
||||
"state": {
|
||||
"assetIds": None
|
||||
}
|
||||
}
|
||||
|
||||
families.active_changed.connect(subsets.set_family_filters)
|
||||
assets.selection_changed.connect(self.on_assetschanged)
|
||||
assets.refresh_triggered.connect(self.on_assetschanged)
|
||||
assets.view.clicked.connect(self.on_assetview_click)
|
||||
subsets.active_changed.connect(self.on_subsetschanged)
|
||||
subsets.version_changed.connect(self.on_versionschanged)
|
||||
self.combo_projects.currentTextChanged.connect(self.on_project_change)
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
# Set default thumbnail on start
|
||||
thumbnail.set_thumbnail(None)
|
||||
|
||||
# Defaults
|
||||
if sync_server.enabled:
|
||||
split.setSizes([250, 1000, 550])
|
||||
self.resize(1800, 900)
|
||||
else:
|
||||
split.setSizes([250, 850, 200])
|
||||
self.resize(1300, 700)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LibraryLoaderWindow, self).showEvent(event)
|
||||
if not self._initial_refresh:
|
||||
self.refresh()
|
||||
|
||||
def on_assetview_click(self, *args):
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
selection_model = subsets_widget.view.selectionModel()
|
||||
if selection_model.selectedIndexes():
|
||||
selection_model.clearSelection()
|
||||
|
||||
def _set_projects(self):
|
||||
# Store current project
|
||||
old_project_name = self.current_project
|
||||
|
||||
self._ignore_project_change = True
|
||||
|
||||
# Cleanup
|
||||
self.combo_projects.clear()
|
||||
|
||||
# Fill combobox with projects
|
||||
select_project_item = QtGui.QStandardItem("< Select project >")
|
||||
select_project_item.setData(None, QtCore.Qt.UserRole + 1)
|
||||
|
||||
combobox_items = [select_project_item]
|
||||
|
||||
project_names = self.get_filtered_projects()
|
||||
|
||||
for project_name in sorted(project_names):
|
||||
item = QtGui.QStandardItem(project_name)
|
||||
item.setData(project_name, QtCore.Qt.UserRole + 1)
|
||||
combobox_items.append(item)
|
||||
|
||||
root_item = self.combo_projects.model().invisibleRootItem()
|
||||
root_item.appendRows(combobox_items)
|
||||
|
||||
index = 0
|
||||
self._ignore_project_change = False
|
||||
|
||||
if old_project_name:
|
||||
index = self.combo_projects.findText(
|
||||
old_project_name, QtCore.Qt.MatchFixedString
|
||||
)
|
||||
|
||||
self.combo_projects.setCurrentIndex(index)
|
||||
|
||||
def get_filtered_projects(self):
|
||||
projects = list()
|
||||
for project in self.dbcon.projects():
|
||||
is_library = project.get("data", {}).get("library_project", False)
|
||||
if (
|
||||
(is_library and self.show_libraries) or
|
||||
(not is_library and self.show_projects)
|
||||
):
|
||||
projects.append(project["name"])
|
||||
|
||||
return projects
|
||||
|
||||
def on_project_change(self):
|
||||
if self._ignore_project_change:
|
||||
return
|
||||
|
||||
row = self.combo_projects.currentIndex()
|
||||
index = self.combo_projects.model().index(row, 0)
|
||||
project_name = index.data(QtCore.Qt.UserRole + 1)
|
||||
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
_config = lib.find_config()
|
||||
if hasattr(_config, "install"):
|
||||
_config.install()
|
||||
else:
|
||||
print(
|
||||
"Config `%s` has no function `install`" % _config.__name__
|
||||
)
|
||||
|
||||
self.family_config_cache.refresh()
|
||||
self.groups_config.refresh()
|
||||
|
||||
self._refresh_assets()
|
||||
self._assetschanged()
|
||||
|
||||
project_name = self.dbcon.active_project() or "No project selected"
|
||||
title = "{} - {}".format(self.tool_title, project_name)
|
||||
self.setWindowTitle(title)
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
|
||||
|
||||
@property
|
||||
def current_project(self):
|
||||
if (
|
||||
not self.dbcon.active_project() or
|
||||
self.dbcon.active_project() == ""
|
||||
):
|
||||
return None
|
||||
|
||||
return self.dbcon.active_project()
|
||||
|
||||
# -------------------------------
|
||||
# Delay calling blocking methods
|
||||
# -------------------------------
|
||||
|
||||
def refresh(self):
|
||||
self.echo("Fetching results..")
|
||||
tools_lib.schedule(self._refresh, 50, channel="mongo")
|
||||
|
||||
def on_assetschanged(self, *args):
|
||||
self.echo("Fetching asset..")
|
||||
tools_lib.schedule(self._assetschanged, 50, channel="mongo")
|
||||
|
||||
def on_subsetschanged(self, *args):
|
||||
self.echo("Fetching subset..")
|
||||
tools_lib.schedule(self._subsetschanged, 50, channel="mongo")
|
||||
|
||||
def on_versionschanged(self, *args):
|
||||
self.echo("Fetching version..")
|
||||
tools_lib.schedule(self._versionschanged, 150, channel="mongo")
|
||||
|
||||
def set_context(self, context, refresh=True):
|
||||
self.echo("Setting context: {}".format(context))
|
||||
lib.schedule(
|
||||
lambda: self._set_context(context, refresh=refresh),
|
||||
50, channel="mongo"
|
||||
)
|
||||
|
||||
# ------------------------------
|
||||
def _refresh(self):
|
||||
if not self._initial_refresh:
|
||||
self._initial_refresh = True
|
||||
self._set_projects()
|
||||
|
||||
def _refresh_assets(self):
|
||||
"""Load assets from database"""
|
||||
if self.current_project is not None:
|
||||
# Ensure a project is loaded
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
{"type": 1}
|
||||
)
|
||||
assert project_doc, "This is a bug"
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.model.stop_fetch_thread()
|
||||
assets_widget.refresh()
|
||||
assets_widget.setFocus()
|
||||
|
||||
families = self.data["widgets"]["families"]
|
||||
families.refresh()
|
||||
|
||||
def clear_assets_underlines(self):
|
||||
last_asset_ids = self.data["state"]["assetIds"]
|
||||
if not last_asset_ids:
|
||||
return
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
id_role = assets_widget.model.ObjectIdRole
|
||||
|
||||
for index in tools_lib.iter_model_rows(assets_widget.model, 0):
|
||||
if index.data(id_role) not in last_asset_ids:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, [], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
|
||||
def _assetschanged(self):
|
||||
"""Selected assets have changed"""
|
||||
t1 = time.time()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
subsets_model = subsets_widget.model
|
||||
|
||||
subsets_model.clear()
|
||||
self.clear_assets_underlines()
|
||||
|
||||
if not self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
subsets_widget.set_loading_state(
|
||||
loading=False,
|
||||
empty=True
|
||||
)
|
||||
return
|
||||
|
||||
# filter None docs they are silo
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if len(asset_docs) == 0:
|
||||
return
|
||||
|
||||
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
|
||||
# Start loading
|
||||
subsets_widget.set_loading_state(
|
||||
loading=bool(asset_ids),
|
||||
empty=True
|
||||
)
|
||||
|
||||
def on_refreshed(has_item):
|
||||
empty = not has_item
|
||||
subsets_widget.set_loading_state(loading=False, empty=empty)
|
||||
subsets_model.refreshed.disconnect()
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
subsets_model.refreshed.connect(on_refreshed)
|
||||
|
||||
subsets_model.set_assets(asset_ids)
|
||||
subsets_widget.view.setColumnHidden(
|
||||
subsets_model.Columns.index("asset"),
|
||||
len(asset_ids) < 2
|
||||
)
|
||||
|
||||
# Clear the version information on asset change
|
||||
self.data["widgets"]["version"].set_version(None)
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
|
||||
|
||||
self.data["state"]["assetIds"] = asset_ids
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.set_version_ids([]) # reset repre list
|
||||
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
def _subsetschanged(self):
|
||||
asset_ids = self.data["state"]["assetIds"]
|
||||
# Skip setting colors if not asset multiselection
|
||||
if not asset_ids or len(asset_ids) < 2:
|
||||
self._versionschanged()
|
||||
return
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
|
||||
|
||||
asset_models = {}
|
||||
asset_ids = []
|
||||
for subset_node in selected_subsets:
|
||||
asset_ids.extend(subset_node.get("assetIds", []))
|
||||
asset_ids = set(asset_ids)
|
||||
|
||||
for subset_node in selected_subsets:
|
||||
for asset_id in asset_ids:
|
||||
if asset_id not in asset_models:
|
||||
asset_models[asset_id] = []
|
||||
|
||||
color = None
|
||||
if asset_id in subset_node.get("assetIds", []):
|
||||
color = subset_node["subsetColor"]
|
||||
|
||||
asset_models[asset_id].append(color)
|
||||
|
||||
self.clear_assets_underlines()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
indexes = assets_widget.view.selectionModel().selectedRows()
|
||||
|
||||
for index in indexes:
|
||||
id = index.data(assets_widget.model.ObjectIdRole)
|
||||
if id not in asset_models:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, asset_models[id], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
# Trigger repaint
|
||||
assets_widget.view.updateGeometries()
|
||||
# Set version in Version Widget
|
||||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selection = subsets.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
version_docs = None
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active and active in rows:
|
||||
item = active.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is not None
|
||||
and not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
|
||||
if rows:
|
||||
version_docs = []
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is None
|
||||
or item.get("isGroup")
|
||||
or item.get("isMerged")
|
||||
):
|
||||
continue
|
||||
version_docs.append(item["version_document"])
|
||||
|
||||
self.data["widgets"]["version"].set_version(version_doc)
|
||||
|
||||
thumbnail_docs = version_docs
|
||||
if not thumbnail_docs:
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if len(asset_docs) > 0:
|
||||
thumbnail_docs = asset_docs
|
||||
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
version_ids = [doc["_id"] for doc in version_docs or []]
|
||||
representations.set_version_ids(version_ids)
|
||||
|
||||
def _set_context(self, context, refresh=True):
|
||||
"""Set the selection in the interface using a context.
|
||||
The context must contain `asset` data by name.
|
||||
Note: Prior to setting context ensure `refresh` is triggered so that
|
||||
the "silos" are listed correctly, aside from that setting the
|
||||
context will force a refresh further down because it changes
|
||||
the active silo and asset.
|
||||
Args:
|
||||
context (dict): The context to apply.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
asset = context.get("asset", None)
|
||||
if asset is None:
|
||||
return
|
||||
|
||||
if refresh:
|
||||
# Workaround:
|
||||
# Force a direct (non-scheduled) refresh prior to setting the
|
||||
# asset widget's silo and asset selection to ensure it's correctly
|
||||
# displaying the silo tabs. Calling `window.refresh()` and directly
|
||||
# `window.set_context()` the `set_context()` seems to override the
|
||||
# scheduled refresh and the silo tabs are not shown.
|
||||
self._refresh_assets()
|
||||
|
||||
asset_widget = self.data["widgets"]["assets"]
|
||||
asset_widget.select_assets(asset)
|
||||
|
||||
def echo(self, message):
|
||||
widget = self.data["label"]["message"]
|
||||
widget.setText(str(message))
|
||||
widget.show()
|
||||
print(message)
|
||||
|
||||
tools_lib.schedule(widget.hide, 5000, channel="message")
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Kill on holding SHIFT
|
||||
modifiers = QtWidgets.QApplication.queryKeyboardModifiers()
|
||||
shift_pressed = QtCore.Qt.ShiftModifier & modifiers
|
||||
|
||||
if shift_pressed:
|
||||
print("Force quitted..")
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
print("Good bye")
|
||||
return super(LibraryLoaderWindow, self).closeEvent(event)
|
||||
|
||||
|
||||
def show(
|
||||
debug=False, parent=None, icon=None,
|
||||
show_projects=False, show_libraries=True
|
||||
):
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): The Qt object to parent to.
|
||||
use_context (bool): Whether to apply the current context upon launch
|
||||
|
||||
"""
|
||||
# Remember window
|
||||
if module.window is not None:
|
||||
try:
|
||||
module.window.show()
|
||||
|
||||
# If the window is minimized then unminimize it.
|
||||
if module.window.windowState() & QtCore.Qt.WindowMinimized:
|
||||
module.window.setWindowState(QtCore.Qt.WindowActive)
|
||||
|
||||
# Raise and activate the window
|
||||
module.window.raise_() # for MacOS
|
||||
module.window.activateWindow() # for Windows
|
||||
module.window.refresh()
|
||||
return
|
||||
except RuntimeError as e:
|
||||
if not e.message.rstrip().endswith("already deleted."):
|
||||
raise
|
||||
|
||||
# Garbage collected
|
||||
module.window = None
|
||||
|
||||
if debug:
|
||||
import traceback
|
||||
sys.excepthook = lambda typ, val, tb: traceback.print_last()
|
||||
|
||||
with tools_lib.application():
|
||||
window = LibraryLoaderWindow(
|
||||
parent, icon, show_projects, show_libraries
|
||||
)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
|
||||
def cli(args):
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project")
|
||||
|
||||
show(show_projects=True, show_libraries=True)
|
||||
33
openpype/tools/libraryloader/lib.py
Normal file
33
openpype/tools/libraryloader/lib.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
import importlib
|
||||
import logging
|
||||
from openpype.api import Anatomy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# `find_config` from `pipeline`
|
||||
def find_config():
|
||||
log.info("Finding configuration for project..")
|
||||
|
||||
config = os.environ["AVALON_CONFIG"]
|
||||
|
||||
if not config:
|
||||
raise EnvironmentError(
|
||||
"No configuration found in "
|
||||
"the project nor environment"
|
||||
)
|
||||
|
||||
log.info("Found %s, loading.." % config)
|
||||
return importlib.import_module(config)
|
||||
|
||||
|
||||
class RegisteredRoots:
|
||||
roots_per_project = {}
|
||||
|
||||
@classmethod
|
||||
def registered_root(cls, project_name):
|
||||
if project_name not in cls.roots_per_project:
|
||||
cls.roots_per_project[project_name] = Anatomy(project_name).roots
|
||||
|
||||
return cls.roots_per_project[project_name]
|
||||
18
openpype/tools/libraryloader/widgets.py
Normal file
18
openpype/tools/libraryloader/widgets.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from .lib import RegisteredRoots
|
||||
from openpype.tools.loader.widgets import SubsetWidget
|
||||
|
||||
|
||||
class LibrarySubsetWidget(SubsetWidget):
|
||||
def on_copy_source(self):
|
||||
"""Copy formatted source path to clipboard"""
|
||||
source = self.data.get("source", None)
|
||||
if not source:
|
||||
return
|
||||
|
||||
project_name = self.dbcon.Session["AVALON_PROJECT"]
|
||||
root = RegisteredRoots.registered_root(project_name)
|
||||
path = source.format(root=root)
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(path)
|
||||
11
openpype/tools/loader/__init__.py
Normal file
11
openpype/tools/loader/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from .app import (
|
||||
LoaderWindow,
|
||||
show,
|
||||
cli,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"LoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
)
|
||||
33
openpype/tools/loader/__main__.py
Normal file
33
openpype/tools/loader/__main__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Main entrypoint for standalone debugging
|
||||
|
||||
Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for
|
||||
debugging without need to start host.
|
||||
|
||||
Modify AVALON_MONGO accordingly
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from . import cli
|
||||
|
||||
|
||||
def my_exception_hook(exctype, value, traceback):
|
||||
# Print the error and traceback
|
||||
print(exctype, value, traceback)
|
||||
# Call the normal Exception hook after
|
||||
sys._excepthook(exctype, value, traceback)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ["AVALON_MONGO"] = "mongodb://localhost:27017"
|
||||
os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017"
|
||||
os.environ["AVALON_DB"] = "avalon"
|
||||
os.environ["AVALON_TIMEOUT"] = "1000"
|
||||
os.environ["OPENPYPE_DEBUG"] = "1"
|
||||
os.environ["AVALON_CONFIG"] = "pype"
|
||||
os.environ["AVALON_ASSET"] = "Jungle"
|
||||
|
||||
# Set the exception hook to our wrapping function
|
||||
sys.excepthook = my_exception_hook
|
||||
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
674
openpype/tools/loader/app.py
Normal file
674
openpype/tools/loader/app.py
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
import sys
|
||||
import time
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from avalon import api, io, style, pipeline
|
||||
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
|
||||
from openpype.tools.utils import lib
|
||||
|
||||
from .widgets import (
|
||||
SubsetWidget,
|
||||
VersionWidget,
|
||||
FamilyListWidget,
|
||||
ThumbnailWidget,
|
||||
RepresentationWidget,
|
||||
OverlayFrame
|
||||
)
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
# Register callback on task change
|
||||
# - callback can't be defined in Window as it is weak reference callback
|
||||
# so `WeakSet` will remove it immidiatelly
|
||||
def on_context_task_change(*args, **kwargs):
|
||||
if module.window:
|
||||
module.window.on_context_task_change(*args, **kwargs)
|
||||
|
||||
|
||||
pipeline.on("taskChanged", on_context_task_change)
|
||||
|
||||
|
||||
class LoaderWindow(QtWidgets.QDialog):
|
||||
"""Asset loader interface"""
|
||||
|
||||
tool_name = "loader"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LoaderWindow, self).__init__(parent)
|
||||
title = "Asset Loader 2.1"
|
||||
project_name = api.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
title += " - {}".format(project_name)
|
||||
self.setWindowTitle(title)
|
||||
|
||||
# Groups config
|
||||
self.groups_config = lib.GroupsConfig(io)
|
||||
self.family_config_cache = lib.FamilyConfigCache(io)
|
||||
|
||||
# Enable minimize and maximize for app
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = QtWidgets.QWidget()
|
||||
|
||||
assets = AssetWidget(io, multiselection=True, parent=self)
|
||||
assets.set_current_asset_btn_visibility(True)
|
||||
|
||||
families = FamilyListWidget(io, self.family_config_cache, self)
|
||||
subsets = SubsetWidget(
|
||||
io,
|
||||
self.groups_config,
|
||||
self.family_config_cache,
|
||||
tool_name=self.tool_name,
|
||||
parent=self
|
||||
)
|
||||
version = VersionWidget(io)
|
||||
thumbnail = ThumbnailWidget(io)
|
||||
representations = RepresentationWidget(io, self.tool_name)
|
||||
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
|
||||
thumb_ver_splitter = QtWidgets.QSplitter()
|
||||
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
thumb_ver_splitter.addWidget(thumbnail)
|
||||
thumb_ver_splitter.addWidget(version)
|
||||
if sync_server.enabled:
|
||||
thumb_ver_splitter.addWidget(representations)
|
||||
thumb_ver_splitter.setStretchFactor(0, 30)
|
||||
thumb_ver_splitter.setStretchFactor(1, 35)
|
||||
|
||||
# Create splitter to show / hide family filters
|
||||
asset_filter_splitter = QtWidgets.QSplitter()
|
||||
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
asset_filter_splitter.addWidget(assets)
|
||||
asset_filter_splitter.addWidget(families)
|
||||
asset_filter_splitter.setStretchFactor(0, 65)
|
||||
asset_filter_splitter.setStretchFactor(1, 35)
|
||||
|
||||
container_layout = QtWidgets.QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
split = QtWidgets.QSplitter()
|
||||
split.addWidget(asset_filter_splitter)
|
||||
split.addWidget(subsets)
|
||||
split.addWidget(thumb_ver_splitter)
|
||||
|
||||
container_layout.addWidget(split)
|
||||
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.data = {
|
||||
"widgets": {
|
||||
"families": families,
|
||||
"assets": assets,
|
||||
"subsets": subsets,
|
||||
"version": version,
|
||||
"thumbnail": thumbnail,
|
||||
"representations": representations
|
||||
},
|
||||
"label": {
|
||||
"message": message,
|
||||
},
|
||||
"state": {
|
||||
"assetIds": None
|
||||
}
|
||||
}
|
||||
|
||||
overlay_frame = OverlayFrame("Loading...", self)
|
||||
overlay_frame.setVisible(False)
|
||||
|
||||
families.active_changed.connect(subsets.set_family_filters)
|
||||
assets.selection_changed.connect(self.on_assetschanged)
|
||||
assets.refresh_triggered.connect(self.on_assetschanged)
|
||||
assets.view.clicked.connect(self.on_assetview_click)
|
||||
subsets.active_changed.connect(self.on_subsetschanged)
|
||||
subsets.version_changed.connect(self.on_versionschanged)
|
||||
|
||||
subsets.load_started.connect(self._on_load_start)
|
||||
subsets.load_ended.connect(self._on_load_end)
|
||||
representations.load_started.connect(self._on_load_start)
|
||||
representations.load_ended.connect(self._on_load_end)
|
||||
|
||||
self._overlay_frame = overlay_frame
|
||||
|
||||
self.family_config_cache.refresh()
|
||||
self.groups_config.refresh()
|
||||
|
||||
self._refresh()
|
||||
self._assetschanged()
|
||||
|
||||
# Defaults
|
||||
if sync_server.enabled:
|
||||
split.setSizes([250, 1000, 550])
|
||||
self.resize(1800, 900)
|
||||
else:
|
||||
split.setSizes([250, 850, 200])
|
||||
self.resize(1300, 700)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoaderWindow, self).resizeEvent(event)
|
||||
self._overlay_frame.resize(self.size())
|
||||
|
||||
def moveEvent(self, event):
|
||||
super(LoaderWindow, self).moveEvent(event)
|
||||
self._overlay_frame.move(0, 0)
|
||||
|
||||
# -------------------------------
|
||||
# Delay calling blocking methods
|
||||
# -------------------------------
|
||||
|
||||
def on_assetview_click(self, *args):
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
selection_model = subsets_widget.view.selectionModel()
|
||||
if selection_model.selectedIndexes():
|
||||
selection_model.clearSelection()
|
||||
|
||||
def refresh(self):
|
||||
self.echo("Fetching results..")
|
||||
lib.schedule(self._refresh, 50, channel="mongo")
|
||||
|
||||
def on_assetschanged(self, *args):
|
||||
self.echo("Fetching asset..")
|
||||
lib.schedule(self._assetschanged, 50, channel="mongo")
|
||||
|
||||
def on_subsetschanged(self, *args):
|
||||
self.echo("Fetching subset..")
|
||||
lib.schedule(self._subsetschanged, 50, channel="mongo")
|
||||
|
||||
def on_versionschanged(self, *args):
|
||||
self.echo("Fetching version..")
|
||||
lib.schedule(self._versionschanged, 150, channel="mongo")
|
||||
|
||||
def set_context(self, context, refresh=True):
|
||||
self.echo("Setting context: {}".format(context))
|
||||
lib.schedule(lambda: self._set_context(context, refresh=refresh),
|
||||
50, channel="mongo")
|
||||
|
||||
def _on_load_start(self):
|
||||
# Show overlay and process events so it's repainted
|
||||
self._overlay_frame.setVisible(True)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
def _hide_overlay(self):
|
||||
self._overlay_frame.setVisible(False)
|
||||
|
||||
def _on_load_end(self):
|
||||
# Delay hiding as click events happened during loading should be
|
||||
# blocked
|
||||
QtCore.QTimer.singleShot(100, self._hide_overlay)
|
||||
|
||||
# ------------------------------
|
||||
|
||||
def on_context_task_change(self, *args, **kwargs):
|
||||
# Change to context asset on context change
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.select_assets(io.Session["AVALON_ASSET"])
|
||||
|
||||
def _refresh(self):
|
||||
"""Load assets from database"""
|
||||
|
||||
# Ensure a project is loaded
|
||||
project = io.find_one({"type": "project"}, {"type": 1})
|
||||
assert project, "Project was not found! This is a bug"
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.refresh()
|
||||
assets_widget.setFocus()
|
||||
|
||||
families = self.data["widgets"]["families"]
|
||||
families.refresh()
|
||||
|
||||
def clear_assets_underlines(self):
|
||||
"""Clear colors from asset data to remove colored underlines
|
||||
When multiple assets are selected colored underlines mark which asset
|
||||
own selected subsets. These colors must be cleared from asset data
|
||||
on selection change so they match current selection.
|
||||
"""
|
||||
last_asset_ids = self.data["state"]["assetIds"]
|
||||
if not last_asset_ids:
|
||||
return
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
id_role = assets_widget.model.ObjectIdRole
|
||||
|
||||
for index in lib.iter_model_rows(assets_widget.model, 0):
|
||||
if index.data(id_role) not in last_asset_ids:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, [], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
|
||||
def _assetschanged(self):
|
||||
"""Selected assets have changed"""
|
||||
t1 = time.time()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
subsets_model = subsets_widget.model
|
||||
|
||||
subsets_model.clear()
|
||||
self.clear_assets_underlines()
|
||||
|
||||
# filter None docs they are silo
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
|
||||
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
|
||||
# Start loading
|
||||
subsets_widget.set_loading_state(
|
||||
loading=bool(asset_ids),
|
||||
empty=True
|
||||
)
|
||||
|
||||
def on_refreshed(has_item):
|
||||
empty = not has_item
|
||||
subsets_widget.set_loading_state(loading=False, empty=empty)
|
||||
subsets_model.refreshed.disconnect()
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
subsets_model.refreshed.connect(on_refreshed)
|
||||
|
||||
subsets_model.set_assets(asset_ids)
|
||||
subsets_widget.view.setColumnHidden(
|
||||
subsets_model.Columns.index("asset"),
|
||||
len(asset_ids) < 2
|
||||
)
|
||||
|
||||
# Clear the version information on asset change
|
||||
self.data["widgets"]["version"].set_version(None)
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
|
||||
|
||||
self.data["state"]["assetIds"] = asset_ids
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.set_version_ids([]) # reset repre list
|
||||
|
||||
def _subsetschanged(self):
|
||||
asset_ids = self.data["state"]["assetIds"]
|
||||
# Skip setting colors if not asset multiselection
|
||||
if not asset_ids or len(asset_ids) < 2:
|
||||
self._versionschanged()
|
||||
return
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
|
||||
|
||||
asset_models = {}
|
||||
asset_ids = []
|
||||
for subset_node in selected_subsets:
|
||||
asset_ids.extend(subset_node.get("assetIds", []))
|
||||
asset_ids = set(asset_ids)
|
||||
|
||||
for subset_node in selected_subsets:
|
||||
for asset_id in asset_ids:
|
||||
if asset_id not in asset_models:
|
||||
asset_models[asset_id] = []
|
||||
|
||||
color = None
|
||||
if asset_id in subset_node.get("assetIds", []):
|
||||
color = subset_node["subsetColor"]
|
||||
|
||||
asset_models[asset_id].append(color)
|
||||
|
||||
self.clear_assets_underlines()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
indexes = assets_widget.view.selectionModel().selectedRows()
|
||||
|
||||
for index in indexes:
|
||||
id = index.data(assets_widget.model.ObjectIdRole)
|
||||
if id not in asset_models:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, asset_models[id], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
# Trigger repaint
|
||||
assets_widget.view.updateGeometries()
|
||||
# Set version in Version Widget
|
||||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selection = subsets.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
version_docs = None
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active:
|
||||
if active in rows:
|
||||
item = active.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is not None and
|
||||
not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
|
||||
if rows:
|
||||
version_docs = []
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(subsets.model.ItemRole)
|
||||
if item is None:
|
||||
continue
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
for child in item.children():
|
||||
version_docs.append(child["version_document"])
|
||||
else:
|
||||
version_docs.append(item["version_document"])
|
||||
|
||||
self.data["widgets"]["version"].set_version(version_doc)
|
||||
|
||||
thumbnail_docs = version_docs
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if not thumbnail_docs:
|
||||
if len(asset_docs) > 0:
|
||||
thumbnail_docs = asset_docs
|
||||
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
version_ids = [doc["_id"] for doc in version_docs or []]
|
||||
representations.set_version_ids(version_ids)
|
||||
|
||||
# representations.change_visibility("subset", len(rows) > 1)
|
||||
# representations.change_visibility("asset", len(asset_docs) > 1)
|
||||
|
||||
def _set_context(self, context, refresh=True):
|
||||
"""Set the selection in the interface using a context.
|
||||
|
||||
The context must contain `asset` data by name.
|
||||
|
||||
Note: Prior to setting context ensure `refresh` is triggered so that
|
||||
the "silos" are listed correctly, aside from that setting the
|
||||
context will force a refresh further down because it changes
|
||||
the active silo and asset.
|
||||
|
||||
Args:
|
||||
context (dict): The context to apply.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
asset = context.get("asset", None)
|
||||
if asset is None:
|
||||
return
|
||||
|
||||
if refresh:
|
||||
# Workaround:
|
||||
# Force a direct (non-scheduled) refresh prior to setting the
|
||||
# asset widget's silo and asset selection to ensure it's correctly
|
||||
# displaying the silo tabs. Calling `window.refresh()` and directly
|
||||
# `window.set_context()` the `set_context()` seems to override the
|
||||
# scheduled refresh and the silo tabs are not shown.
|
||||
self._refresh()
|
||||
|
||||
asset_widget = self.data["widgets"]["assets"]
|
||||
asset_widget.select_assets(asset)
|
||||
|
||||
def echo(self, message):
|
||||
widget = self.data["label"]["message"]
|
||||
widget.setText(str(message))
|
||||
widget.show()
|
||||
print(message)
|
||||
|
||||
lib.schedule(widget.hide, 5000, channel="message")
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Kill on holding SHIFT
|
||||
modifiers = QtWidgets.QApplication.queryKeyboardModifiers()
|
||||
shift_pressed = QtCore.Qt.ShiftModifier & modifiers
|
||||
|
||||
if shift_pressed:
|
||||
print("Force quitted..")
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
print("Good bye")
|
||||
return super(LoaderWindow, self).closeEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
modifiers = event.modifiers()
|
||||
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
|
||||
|
||||
# Grouping subsets on pressing Ctrl + G
|
||||
if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and
|
||||
not event.isAutoRepeat()):
|
||||
self.show_grouping_dialog()
|
||||
return
|
||||
|
||||
super(LoaderWindow, self).keyPressEvent(event)
|
||||
event.setAccepted(True) # Avoid interfering other widgets
|
||||
|
||||
def show_grouping_dialog(self):
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
if not subsets.is_groupable():
|
||||
self.echo("Grouping not enabled.")
|
||||
return
|
||||
|
||||
selected = []
|
||||
merged_items = []
|
||||
for item in subsets.selected_subsets(_merged=True):
|
||||
if item.get("isMerged"):
|
||||
merged_items.append(item)
|
||||
else:
|
||||
selected.append(item)
|
||||
|
||||
for merged_item in merged_items:
|
||||
for child_item in merged_item.children():
|
||||
selected.append(child_item)
|
||||
|
||||
if not selected:
|
||||
self.echo("No selected subset.")
|
||||
return
|
||||
|
||||
dialog = SubsetGroupingDialog(
|
||||
items=selected, groups_config=self.groups_config, parent=self
|
||||
)
|
||||
dialog.grouped.connect(self._assetschanged)
|
||||
dialog.show()
|
||||
|
||||
|
||||
class SubsetGroupingDialog(QtWidgets.QDialog):
|
||||
grouped = QtCore.Signal()
|
||||
|
||||
def __init__(self, items, groups_config, parent=None):
|
||||
super(SubsetGroupingDialog, self).__init__(parent=parent)
|
||||
self.setWindowTitle("Grouping Subsets")
|
||||
self.setMinimumWidth(250)
|
||||
self.setModal(True)
|
||||
|
||||
self.items = items
|
||||
self.groups_config = groups_config
|
||||
self.subsets = parent.data["widgets"]["subsets"]
|
||||
self.asset_ids = parent.data["state"]["assetIds"]
|
||||
|
||||
name = QtWidgets.QLineEdit()
|
||||
name.setPlaceholderText("Remain blank to ungroup..")
|
||||
|
||||
# Menu for pre-defined subset groups
|
||||
name_button = QtWidgets.QPushButton()
|
||||
name_button.setFixedWidth(18)
|
||||
name_button.setFixedHeight(20)
|
||||
name_menu = QtWidgets.QMenu(name_button)
|
||||
name_button.setMenu(name_menu)
|
||||
|
||||
name_layout = QtWidgets.QHBoxLayout()
|
||||
name_layout.addWidget(name)
|
||||
name_layout.addWidget(name_button)
|
||||
name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
group_btn = QtWidgets.QPushButton("Apply")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(QtWidgets.QLabel("Group Name"))
|
||||
layout.addLayout(name_layout)
|
||||
layout.addWidget(group_btn)
|
||||
|
||||
group_btn.clicked.connect(self.on_group)
|
||||
group_btn.setAutoDefault(True)
|
||||
group_btn.setDefault(True)
|
||||
|
||||
self.name = name
|
||||
self.name_menu = name_menu
|
||||
|
||||
self._build_menu()
|
||||
|
||||
def _build_menu(self):
|
||||
menu = self.name_menu
|
||||
button = menu.parent()
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
active_groups = self.groups_config.active_groups(self.asset_ids)
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
group_names = list()
|
||||
for data in sorted(active_groups, key=lambda x: x["order"]):
|
||||
name = data["name"]
|
||||
if name in group_names:
|
||||
continue
|
||||
group_names.append(name)
|
||||
icon = data["icon"]
|
||||
|
||||
action = group.addAction(name)
|
||||
action.setIcon(icon)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
button.setEnabled(not menu.isEmpty())
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self.name.setText(action.text())
|
||||
|
||||
def on_group(self):
|
||||
name = self.name.text().strip()
|
||||
self.subsets.group_subsets(name, self.asset_ids, self.items)
|
||||
|
||||
with lib.preserve_selection(tree_view=self.subsets.view,
|
||||
current_index=False):
|
||||
self.grouped.emit()
|
||||
self.close()
|
||||
|
||||
|
||||
def show(debug=False, parent=None, use_context=False):
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): The Qt object to parent to.
|
||||
use_context (bool): Whether to apply the current context upon launch
|
||||
|
||||
"""
|
||||
|
||||
# Remember window
|
||||
if module.window is not None:
|
||||
try:
|
||||
module.window.show()
|
||||
|
||||
# If the window is minimized then unminimize it.
|
||||
if module.window.windowState() & QtCore.Qt.WindowMinimized:
|
||||
module.window.setWindowState(QtCore.Qt.WindowActive)
|
||||
|
||||
# Raise and activate the window
|
||||
module.window.raise_() # for MacOS
|
||||
module.window.activateWindow() # for Windows
|
||||
module.window.refresh()
|
||||
return
|
||||
except (AttributeError, RuntimeError):
|
||||
# Garbage collected
|
||||
module.window = None
|
||||
|
||||
if debug:
|
||||
import traceback
|
||||
sys.excepthook = lambda typ, val, tb: traceback.print_last()
|
||||
|
||||
io.install()
|
||||
|
||||
any_project = next(
|
||||
project for project in io.projects()
|
||||
if project.get("active", True) is not False
|
||||
)
|
||||
|
||||
api.Session["AVALON_PROJECT"] = any_project["name"]
|
||||
module.project = any_project["name"]
|
||||
|
||||
with lib.application():
|
||||
window = LoaderWindow(parent)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
if use_context:
|
||||
context = {"asset": api.Session["AVALON_ASSET"]}
|
||||
window.set_context(context, refresh=True)
|
||||
else:
|
||||
window.refresh()
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
||||
|
||||
def cli(args):
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
project = args.project
|
||||
|
||||
print("Entering Project: %s" % project)
|
||||
|
||||
io.install()
|
||||
|
||||
# Store settings
|
||||
api.Session["AVALON_PROJECT"] = project
|
||||
|
||||
from avalon import pipeline
|
||||
|
||||
# Find the set config
|
||||
_config = pipeline.find_config()
|
||||
if hasattr(_config, "install"):
|
||||
_config.install()
|
||||
else:
|
||||
print("Config `%s` has no function `install`" %
|
||||
_config.__name__)
|
||||
|
||||
show()
|
||||
BIN
openpype/tools/loader/images/default_thumbnail.png
Normal file
BIN
openpype/tools/loader/images/default_thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
190
openpype/tools/loader/lib.py
Normal file
190
openpype/tools/loader/lib.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import inspect
|
||||
from Qt import QtGui
|
||||
|
||||
from avalon.vendor import qtawesome
|
||||
from openpype.tools.utils.widgets import (
|
||||
OptionalAction,
|
||||
OptionDialog
|
||||
)
|
||||
|
||||
|
||||
def change_visibility(model, view, column_name, visible):
|
||||
"""
|
||||
Hides or shows particular 'column_name'.
|
||||
|
||||
"asset" and "subset" columns should be visible only in multiselect
|
||||
"""
|
||||
index = model.Columns.index(column_name)
|
||||
view.setColumnHidden(index, not visible)
|
||||
|
||||
|
||||
def get_selected_items(rows, item_role):
|
||||
items = []
|
||||
for row_index in rows:
|
||||
item = row_index.data(item_role)
|
||||
if item.get("isGroup"):
|
||||
continue
|
||||
|
||||
elif item.get("isMerged"):
|
||||
for idx in range(row_index.model().rowCount(row_index)):
|
||||
child_index = row_index.child(idx, 0)
|
||||
item = child_index.data(item_role)
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
|
||||
else:
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def get_options(action, loader, parent, repre_contexts):
|
||||
"""Provides dialog to select value from loader provided options.
|
||||
|
||||
Loader can provide static or dynamically created options based on
|
||||
qargparse variants.
|
||||
|
||||
Args:
|
||||
action (OptionalAction) - action in menu
|
||||
loader (cls of api.Loader) - not initilized yet
|
||||
parent (Qt element to parent dialog to)
|
||||
repre_contexts (list) of dict with full info about selected repres
|
||||
Returns:
|
||||
(dict) - selected value from OptionDialog
|
||||
None when dialog was closed or cancelled, in all other cases {}
|
||||
if no options
|
||||
"""
|
||||
# Pop option dialog
|
||||
options = {}
|
||||
loader_options = loader.get_options(repre_contexts)
|
||||
if getattr(action, "optioned", False) and loader_options:
|
||||
dialog = OptionDialog(parent)
|
||||
dialog.setWindowTitle(action.label + " Options")
|
||||
dialog.create(loader_options)
|
||||
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
# Get option
|
||||
options = dialog.parse()
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def add_representation_loaders_to_menu(loaders, menu, repre_contexts):
|
||||
"""
|
||||
Loops through provider loaders and adds them to 'menu'.
|
||||
|
||||
Expects loaders sorted in requested order.
|
||||
Expects loaders de-duplicated if wanted.
|
||||
|
||||
Args:
|
||||
loaders(tuple): representation - loader
|
||||
menu (OptionalMenu):
|
||||
repre_contexts (dict): full info about representations (contains
|
||||
their repre_doc, asset_doc, subset_doc, version_doc),
|
||||
keys are repre_ids
|
||||
|
||||
Returns:
|
||||
menu (OptionalMenu): with new items
|
||||
"""
|
||||
# List the available loaders
|
||||
for representation, loader in loaders:
|
||||
label = None
|
||||
repre_context = None
|
||||
if representation:
|
||||
label = representation.get("custom_label")
|
||||
repre_context = repre_contexts[representation["_id"]]
|
||||
|
||||
if not label:
|
||||
label = get_label_from_loader(loader, representation)
|
||||
|
||||
icon = get_icon_from_loader(loader)
|
||||
|
||||
loader_options = loader.get_options([repre_context])
|
||||
|
||||
use_option = bool(loader_options)
|
||||
action = OptionalAction(label, icon, use_option, menu)
|
||||
if use_option:
|
||||
# Add option box tip
|
||||
action.set_option_tip(loader_options)
|
||||
|
||||
action.setData((representation, loader))
|
||||
|
||||
# Add tooltip and statustip from Loader docstring
|
||||
tip = inspect.getdoc(loader)
|
||||
if tip:
|
||||
action.setToolTip(tip)
|
||||
action.setStatusTip(tip)
|
||||
|
||||
menu.addAction(action)
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def remove_tool_name_from_loaders(available_loaders, tool_name):
|
||||
if not tool_name:
|
||||
return available_loaders
|
||||
filtered_loaders = []
|
||||
for loader in available_loaders:
|
||||
if hasattr(loader, "tool_names"):
|
||||
if not ("*" in loader.tool_names or
|
||||
tool_name in loader.tool_names):
|
||||
continue
|
||||
filtered_loaders.append(loader)
|
||||
return filtered_loaders
|
||||
|
||||
|
||||
def get_icon_from_loader(loader):
|
||||
"""Pull icon info from loader class"""
|
||||
# Support font-awesome icons using the `.icon` and `.color`
|
||||
# attributes on plug-ins.
|
||||
icon = getattr(loader, "icon", None)
|
||||
if icon is not None:
|
||||
try:
|
||||
key = "fa.{0}".format(icon)
|
||||
color = getattr(loader, "color", "white")
|
||||
icon = qtawesome.icon(key, color=color)
|
||||
except Exception as e:
|
||||
print("Unable to set icon for loader "
|
||||
"{}: {}".format(loader, e))
|
||||
icon = None
|
||||
return icon
|
||||
|
||||
|
||||
def get_label_from_loader(loader, representation=None):
|
||||
"""Pull label info from loader class"""
|
||||
label = getattr(loader, "label", None)
|
||||
if label is None:
|
||||
label = loader.__name__
|
||||
if representation:
|
||||
# Add the representation as suffix
|
||||
label = "{0} ({1})".format(label, representation['name'])
|
||||
return label
|
||||
|
||||
|
||||
def get_no_loader_action(menu, one_item_selected=False):
|
||||
"""Creates dummy no loader option in 'menu'"""
|
||||
submsg = "your selection."
|
||||
if one_item_selected:
|
||||
submsg = "this version."
|
||||
msg = "No compatible loaders for {}".format(submsg)
|
||||
print(msg)
|
||||
icon = qtawesome.icon(
|
||||
"fa.exclamation",
|
||||
color=QtGui.QColor(255, 51, 0)
|
||||
)
|
||||
action = OptionalAction(("*" + msg), icon, False, menu)
|
||||
return action
|
||||
|
||||
|
||||
def sort_loaders(loaders, custom_sorter=None):
|
||||
def sorter(value):
|
||||
"""Sort the Loaders by their order and then their name"""
|
||||
Plugin = value[1]
|
||||
return Plugin.order, Plugin.__name__
|
||||
|
||||
if not custom_sorter:
|
||||
custom_sorter = sorter
|
||||
|
||||
return sorted(loaders, key=custom_sorter)
|
||||
1191
openpype/tools/loader/model.py
Normal file
1191
openpype/tools/loader/model.py
Normal file
File diff suppressed because it is too large
Load diff
1458
openpype/tools/loader/widgets.py
Normal file
1458
openpype/tools/loader/widgets.py
Normal file
File diff suppressed because it is too large
Load diff
0
openpype/tools/utils/__init__.py
Normal file
0
openpype/tools/utils/__init__.py
Normal file
449
openpype/tools/utils/delegates.py
Normal file
449
openpype/tools/utils/delegates.py
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import numbers
|
||||
|
||||
import Qt
|
||||
from Qt import QtWidgets, QtGui, QtCore
|
||||
|
||||
from avalon.lib import HeroVersionType
|
||||
from .models import (
|
||||
AssetModel,
|
||||
TreeModel
|
||||
)
|
||||
from . import lib
|
||||
|
||||
if Qt.__binding__ == "PySide":
|
||||
from PySide.QtGui import QStyleOptionViewItemV4
|
||||
elif Qt.__binding__ == "PyQt4":
|
||||
from PyQt4.QtGui import QStyleOptionViewItemV4
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssetDelegate(QtWidgets.QItemDelegate):
|
||||
bar_height = 3
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
result = super(AssetDelegate, self).sizeHint(option, index)
|
||||
height = result.height()
|
||||
result.setHeight(height + self.bar_height)
|
||||
|
||||
return result
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
# Qt4 compat
|
||||
if Qt.__binding__ in ("PySide", "PyQt4"):
|
||||
option = QStyleOptionViewItemV4(option)
|
||||
|
||||
painter.save()
|
||||
|
||||
item_rect = QtCore.QRect(option.rect)
|
||||
item_rect.setHeight(option.rect.height() - self.bar_height)
|
||||
|
||||
subset_colors = index.data(AssetModel.subsetColorsRole)
|
||||
subset_colors_width = 0
|
||||
if subset_colors:
|
||||
subset_colors_width = option.rect.width() / len(subset_colors)
|
||||
|
||||
subset_rects = []
|
||||
counter = 0
|
||||
for subset_c in subset_colors:
|
||||
new_color = None
|
||||
new_rect = None
|
||||
if subset_c:
|
||||
new_color = QtGui.QColor(*subset_c)
|
||||
|
||||
new_rect = QtCore.QRect(
|
||||
option.rect.left() + (counter * subset_colors_width),
|
||||
option.rect.top() + (
|
||||
option.rect.height() - self.bar_height
|
||||
),
|
||||
subset_colors_width,
|
||||
self.bar_height
|
||||
)
|
||||
subset_rects.append((new_color, new_rect))
|
||||
counter += 1
|
||||
|
||||
# Background
|
||||
bg_color = QtGui.QColor(60, 60, 60)
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
if len(subset_colors) == 0:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color.setRgb(70, 70, 70)
|
||||
else:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color.setAlpha(100)
|
||||
else:
|
||||
bg_color.setAlpha(0)
|
||||
|
||||
# When not needed to do a rounded corners (easier and without
|
||||
# painter restore):
|
||||
# painter.fillRect(
|
||||
# item_rect,
|
||||
# QtGui.QBrush(bg_color)
|
||||
# )
|
||||
pen = painter.pen()
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
pen.setWidth(0)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtGui.QBrush(bg_color))
|
||||
painter.drawRoundedRect(option.rect, 3, 3)
|
||||
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
for color, subset_rect in subset_rects:
|
||||
if not color or not subset_rect:
|
||||
continue
|
||||
painter.fillRect(subset_rect, QtGui.QBrush(color))
|
||||
|
||||
painter.restore()
|
||||
painter.save()
|
||||
|
||||
# Icon
|
||||
icon_index = index.model().index(
|
||||
index.row(), index.column(), index.parent()
|
||||
)
|
||||
# - Default icon_rect if not icon
|
||||
icon_rect = QtCore.QRect(
|
||||
item_rect.left(),
|
||||
item_rect.top(),
|
||||
# To make sure it's same size all the time
|
||||
option.rect.height() - self.bar_height,
|
||||
option.rect.height() - self.bar_height
|
||||
)
|
||||
icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)
|
||||
|
||||
if icon:
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not (option.state & QtWidgets.QStyle.State_Enabled):
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif option.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
|
||||
if isinstance(icon, QtGui.QPixmap):
|
||||
icon = QtGui.QIcon(icon)
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QColor):
|
||||
pixmap = QtGui.QPixmap(option.decorationSize)
|
||||
pixmap.fill(icon)
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
|
||||
elif isinstance(icon, QtGui.QImage):
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QIcon):
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
actualSize = option.icon.actualSize(
|
||||
option.decorationSize, mode, state
|
||||
)
|
||||
option.decorationSize = QtCore.QSize(
|
||||
min(option.decorationSize.width(), actualSize.width()),
|
||||
min(option.decorationSize.height(), actualSize.height())
|
||||
)
|
||||
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
|
||||
icon.paint(
|
||||
painter, icon_rect,
|
||||
QtCore.Qt.AlignLeft, mode, state
|
||||
)
|
||||
|
||||
# Text
|
||||
text_rect = QtCore.QRect(
|
||||
icon_rect.left() + icon_rect.width() + 2,
|
||||
item_rect.top(),
|
||||
item_rect.width(),
|
||||
item_rect.height()
|
||||
)
|
||||
|
||||
painter.drawText(
|
||||
text_rect, QtCore.Qt.AlignVCenter,
|
||||
index.data(QtCore.Qt.DisplayRole)
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""A delegate that display version integer formatted as version string."""
|
||||
|
||||
version_changed = QtCore.Signal()
|
||||
first_run = False
|
||||
lock = False
|
||||
|
||||
def __init__(self, dbcon, *args, **kwargs):
|
||||
self.dbcon = dbcon
|
||||
super(VersionDelegate, self).__init__(*args, **kwargs)
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if isinstance(value, HeroVersionType):
|
||||
return lib.format_version(value, True)
|
||||
assert isinstance(value, numbers.Integral), (
|
||||
"Version is not integer. \"{}\" {}".format(value, str(type(value)))
|
||||
)
|
||||
return lib.format_version(value)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||
if fg_color:
|
||||
if isinstance(fg_color, QtGui.QBrush):
|
||||
fg_color = fg_color.color()
|
||||
elif isinstance(fg_color, QtGui.QColor):
|
||||
pass
|
||||
else:
|
||||
fg_color = None
|
||||
|
||||
if not fg_color:
|
||||
return super(VersionDelegate, self).paint(painter, option, index)
|
||||
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
else:
|
||||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
style.CE_ItemViewItem, option, painter, option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
||||
text = self.displayText(
|
||||
index.data(QtCore.Qt.DisplayRole), option.locale
|
||||
)
|
||||
pen = painter.pen()
|
||||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
||||
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
style.PM_FocusFrameHMargin, option, option.widget
|
||||
) + 1
|
||||
|
||||
painter.drawText(
|
||||
text_rect.adjusted(text_margin, 0, - text_margin, 0),
|
||||
option.displayAlignment,
|
||||
text
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
item = index.data(TreeModel.ItemRole)
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
return
|
||||
|
||||
editor = QtWidgets.QComboBox(parent)
|
||||
|
||||
def commit_data():
|
||||
if not self.first_run:
|
||||
self.commitData.emit(editor) # Update model data
|
||||
self.version_changed.emit() # Display model data
|
||||
editor.currentIndexChanged.connect(commit_data)
|
||||
|
||||
self.first_run = True
|
||||
self.lock = False
|
||||
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
if self.lock:
|
||||
# Only set editor data once per delegation
|
||||
return
|
||||
|
||||
editor.clear()
|
||||
|
||||
# Current value of the index
|
||||
item = index.data(TreeModel.ItemRole)
|
||||
value = index.data(QtCore.Qt.DisplayRole)
|
||||
if item["version_document"]["type"] != "hero_version":
|
||||
assert isinstance(value, numbers.Integral), (
|
||||
"Version is not integer"
|
||||
)
|
||||
|
||||
# Add all available versions to the editor
|
||||
parent_id = item["version_document"]["parent"]
|
||||
version_docs = list(self.dbcon.find(
|
||||
{
|
||||
"type": "version",
|
||||
"parent": parent_id
|
||||
},
|
||||
sort=[("name", 1)]
|
||||
))
|
||||
|
||||
hero_version_doc = self.dbcon.find_one(
|
||||
{
|
||||
"type": "hero_version",
|
||||
"parent": parent_id
|
||||
}, {
|
||||
"name": 1,
|
||||
"data.tags": 1,
|
||||
"version_id": 1
|
||||
}
|
||||
)
|
||||
|
||||
doc_for_hero_version = None
|
||||
|
||||
selected = None
|
||||
items = []
|
||||
for version_doc in version_docs:
|
||||
version_tags = version_doc["data"].get("tags") or []
|
||||
if "deleted" in version_tags:
|
||||
continue
|
||||
|
||||
if (
|
||||
hero_version_doc
|
||||
and doc_for_hero_version is None
|
||||
and hero_version_doc["version_id"] == version_doc["_id"]
|
||||
):
|
||||
doc_for_hero_version = version_doc
|
||||
|
||||
label = lib.format_version(version_doc["name"])
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(version_doc, QtCore.Qt.UserRole)
|
||||
items.append(item)
|
||||
|
||||
if version_doc["name"] == value:
|
||||
selected = item
|
||||
|
||||
if hero_version_doc and doc_for_hero_version:
|
||||
version_name = doc_for_hero_version["name"]
|
||||
label = lib.format_version(version_name, True)
|
||||
if isinstance(value, HeroVersionType):
|
||||
index = len(version_docs)
|
||||
hero_version_doc["name"] = HeroVersionType(version_name)
|
||||
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(hero_version_doc, QtCore.Qt.UserRole)
|
||||
items.append(item)
|
||||
|
||||
# Reverse items so latest versions be upper
|
||||
items = list(reversed(items))
|
||||
for item in items:
|
||||
editor.model().appendRow(item)
|
||||
|
||||
index = 0
|
||||
if selected:
|
||||
index = selected.row()
|
||||
|
||||
# Will trigger index-change signal
|
||||
editor.setCurrentIndex(index)
|
||||
self.first_run = False
|
||||
self.lock = True
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
"""Apply the integer version back in the model"""
|
||||
version = editor.itemData(editor.currentIndex())
|
||||
model.setData(index, version["name"])
|
||||
|
||||
|
||||
def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"):
|
||||
"""Parse datetime to readable timestamp
|
||||
|
||||
Within first ten seconds:
|
||||
- "just now",
|
||||
Within first minute ago:
|
||||
- "%S seconds ago"
|
||||
Within one hour ago:
|
||||
- "%M minutes ago".
|
||||
Within one day ago:
|
||||
- "%H:%M hours ago"
|
||||
Else:
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
|
||||
"""
|
||||
|
||||
assert isinstance(t, datetime)
|
||||
if now is None:
|
||||
now = datetime.now()
|
||||
assert isinstance(now, datetime)
|
||||
diff = now - t
|
||||
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
|
||||
# future (consider as just now)
|
||||
if day_diff < 0:
|
||||
return "just now"
|
||||
|
||||
# history
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return "just now"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds ago"
|
||||
if second_diff < 120:
|
||||
return "a minute ago"
|
||||
if second_diff < 3600:
|
||||
return str(second_diff // 60) + " minutes ago"
|
||||
if second_diff < 86400:
|
||||
minutes = (second_diff % 3600) // 60
|
||||
hours = second_diff // 3600
|
||||
return "{0}:{1:02d} hours ago".format(hours, minutes)
|
||||
|
||||
return t.strftime(strftime)
|
||||
|
||||
|
||||
def pretty_timestamp(t, now=None):
|
||||
"""Parse timestamp to user readable format
|
||||
|
||||
>>> pretty_timestamp("20170614T151122Z", now="20170614T151123Z")
|
||||
'just now'
|
||||
|
||||
>>> pretty_timestamp("20170614T151122Z", now="20170614T171222Z")
|
||||
'2:01 hours ago'
|
||||
|
||||
Args:
|
||||
t (str): The time string to parse.
|
||||
now (str, optional)
|
||||
|
||||
Returns:
|
||||
str: human readable "recent" date.
|
||||
|
||||
"""
|
||||
|
||||
if now is not None:
|
||||
try:
|
||||
now = time.strptime(now, "%Y%m%dT%H%M%SZ")
|
||||
now = datetime.fromtimestamp(time.mktime(now))
|
||||
except ValueError as e:
|
||||
log.warning("Can't parse 'now' time format: {0} {1}".format(t, e))
|
||||
return None
|
||||
|
||||
if isinstance(t, float):
|
||||
dt = datetime.fromtimestamp(t)
|
||||
else:
|
||||
# Parse the time format as if it is `str` result from
|
||||
# `pyblish.lib.time()` which usually is stored in Avalon database.
|
||||
try:
|
||||
t = time.strptime(t, "%Y%m%dT%H%M%SZ")
|
||||
except ValueError as e:
|
||||
log.warning("Can't parse time format: {0} {1}".format(t, e))
|
||||
return None
|
||||
dt = datetime.fromtimestamp(time.mktime(t))
|
||||
|
||||
# prettify
|
||||
return pretty_date(dt, now=now)
|
||||
|
||||
|
||||
class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""A delegate that displays a timestamp as a pretty date.
|
||||
|
||||
This displays dates like `pretty_date`.
|
||||
|
||||
"""
|
||||
|
||||
def displayText(self, value, locale):
|
||||
|
||||
if value is None:
|
||||
# Ignore None value
|
||||
return
|
||||
|
||||
return pretty_timestamp(value)
|
||||
622
openpype/tools/utils/lib.py
Normal file
622
openpype/tools/utils/lib.py
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
import os
|
||||
import sys
|
||||
import contextlib
|
||||
import collections
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import io, api, style
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._jobs = dict()
|
||||
|
||||
|
||||
class SharedObjects:
|
||||
# Variable for family cache in global context
|
||||
# QUESTION is this safe? More than one tool can refresh at the same time.
|
||||
family_cache = None
|
||||
|
||||
|
||||
def global_family_cache():
|
||||
if SharedObjects.family_cache is None:
|
||||
SharedObjects.family_cache = FamilyConfigCache(io)
|
||||
return SharedObjects.family_cache
|
||||
|
||||
|
||||
def format_version(value, hero_version=False):
|
||||
"""Formats integer to displayable version name"""
|
||||
label = "v{0:03d}".format(value)
|
||||
if not hero_version:
|
||||
return label
|
||||
return "[{}]".format(label)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def application():
|
||||
app = QtWidgets.QApplication.instance()
|
||||
|
||||
if not app:
|
||||
print("Starting new QApplication..")
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
yield app
|
||||
app.exec_()
|
||||
else:
|
||||
print("Using existing QApplication..")
|
||||
yield app
|
||||
|
||||
|
||||
def defer(delay, func):
|
||||
"""Append artificial delay to `func`
|
||||
|
||||
This aids in keeping the GUI responsive, but complicates logic
|
||||
when producing tests. To combat this, the environment variable ensures
|
||||
that every operation is synchonous.
|
||||
|
||||
Arguments:
|
||||
delay (float): Delay multiplier; default 1, 0 means no delay
|
||||
func (callable): Any callable
|
||||
|
||||
"""
|
||||
|
||||
delay *= float(os.getenv("PYBLISH_DELAY", 1))
|
||||
if delay > 0:
|
||||
return QtCore.QTimer.singleShot(delay, func)
|
||||
else:
|
||||
return func()
|
||||
|
||||
|
||||
def schedule(func, time, channel="default"):
|
||||
"""Run `func` at a later `time` in a dedicated `channel`
|
||||
|
||||
Given an arbitrary function, call this function after a given
|
||||
timeout. It will ensure that only one "job" is running within
|
||||
the given channel at any one time and cancel any currently
|
||||
running job if a new job is submitted before the timeout.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
self._jobs[channel].stop()
|
||||
except (AttributeError, KeyError, RuntimeError):
|
||||
pass
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(func)
|
||||
timer.start(time)
|
||||
|
||||
self._jobs[channel] = timer
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def dummy():
|
||||
"""Dummy context manager
|
||||
|
||||
Usage:
|
||||
>> with some_context() if False else dummy():
|
||||
.. pass
|
||||
|
||||
"""
|
||||
|
||||
yield
|
||||
|
||||
|
||||
def iter_model_rows(model, column, include_root=False):
|
||||
"""Iterate over all row indices in a model"""
|
||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
||||
|
||||
for index in indices:
|
||||
# Add children to the iterations
|
||||
child_rows = model.rowCount(index)
|
||||
for child_row in range(child_rows):
|
||||
child_index = model.index(child_row, column, index)
|
||||
indices.append(child_index)
|
||||
|
||||
if not include_root and not index.isValid():
|
||||
continue
|
||||
|
||||
yield index
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_states(tree_view,
|
||||
column=0,
|
||||
role=None,
|
||||
preserve_expanded=True,
|
||||
preserve_selection=True,
|
||||
expanded_role=QtCore.Qt.DisplayRole,
|
||||
selection_role=QtCore.Qt.DisplayRole):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# When `role` is set then override both expanded and selection roles
|
||||
if role:
|
||||
expanded_role = role
|
||||
selection_role = role
|
||||
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
expanded = set()
|
||||
|
||||
if preserve_expanded:
|
||||
for index in iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(expanded_role)
|
||||
expanded.add(value)
|
||||
|
||||
selected = None
|
||||
|
||||
if preserve_selection:
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if selected_rows:
|
||||
selected = set(row.data(selection_role) for row in selected_rows)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if expanded:
|
||||
for index in iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
value = index.data(expanded_role)
|
||||
is_expanded = value in expanded
|
||||
# skip if new index was created meanwhile
|
||||
if is_expanded is None:
|
||||
continue
|
||||
tree_view.setExpanded(index, is_expanded)
|
||||
|
||||
if selected:
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
value = index.data(selection_role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_expanded_rows(tree_view, column=0, role=None):
|
||||
"""Preserves expanded row in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the expand vs collapse status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
Arguments:
|
||||
tree_view (QWidgets.QTreeView): the tree view which is
|
||||
nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
model = tree_view.model()
|
||||
|
||||
expanded = set()
|
||||
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(role)
|
||||
expanded.add(value)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not expanded:
|
||||
return
|
||||
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
value = index.data(role)
|
||||
state = value in expanded
|
||||
if state:
|
||||
tree_view.expand(index)
|
||||
else:
|
||||
tree_view.collapse(index)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_selection(tree_view, column=0, role=None, current_index=True):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
if current_index:
|
||||
current_index_value = tree_view.currentIndex().data(role)
|
||||
else:
|
||||
current_index_value = None
|
||||
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if not selected_rows:
|
||||
yield
|
||||
return
|
||||
|
||||
selected = set(row.data(role) for row in selected_rows)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not selected:
|
||||
return
|
||||
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
value = index.data(role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
||||
if current_index_value and value == current_index_value:
|
||||
selection_model.setCurrentIndex(
|
||||
index, selection_model.NoUpdate
|
||||
)
|
||||
|
||||
|
||||
class FamilyConfigCache:
|
||||
default_color = "#0091B2"
|
||||
_default_icon = None
|
||||
_default_item = None
|
||||
|
||||
def __init__(self, dbcon):
|
||||
self.dbcon = dbcon
|
||||
self.family_configs = {}
|
||||
|
||||
@classmethod
|
||||
def default_icon(cls):
|
||||
if cls._default_icon is None:
|
||||
cls._default_icon = qtawesome.icon(
|
||||
"fa.folder", color=cls.default_color
|
||||
)
|
||||
return cls._default_icon
|
||||
|
||||
@classmethod
|
||||
def default_item(cls):
|
||||
if cls._default_item is None:
|
||||
cls._default_item = {"icon": cls.default_icon()}
|
||||
return cls._default_item
|
||||
|
||||
def family_config(self, family_name):
|
||||
"""Get value from config with fallback to default"""
|
||||
return self.family_configs.get(family_name, self.default_item())
|
||||
|
||||
def refresh(self):
|
||||
"""Get the family configurations from the database
|
||||
|
||||
The configuration must be stored on the project under `config`.
|
||||
For example:
|
||||
|
||||
{"config": {
|
||||
"families": [
|
||||
{"name": "avalon.camera", label: "Camera", "icon": "photo"},
|
||||
{"name": "avalon.anim", label: "Animation", "icon": "male"},
|
||||
]
|
||||
}}
|
||||
|
||||
It is possible to override the default behavior and set specific
|
||||
families checked. For example we only want the families imagesequence
|
||||
and camera to be visible in the Loader.
|
||||
|
||||
# This will turn every item off
|
||||
api.data["familyStateDefault"] = False
|
||||
|
||||
# Only allow the imagesequence and camera
|
||||
api.data["familyStateToggled"] = ["imagesequence", "camera"]
|
||||
|
||||
"""
|
||||
|
||||
self.family_configs.clear()
|
||||
|
||||
families = []
|
||||
|
||||
# Update the icons from the project configuration
|
||||
project_name = self.dbcon.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
projection={"config.families": True}
|
||||
)
|
||||
|
||||
if not project_doc:
|
||||
print((
|
||||
"Project \"{}\" not found!"
|
||||
" Can't refresh family icons cache."
|
||||
).format(project_name))
|
||||
else:
|
||||
families = project_doc["config"].get("families") or []
|
||||
|
||||
# Check if any family state are being overwritten by the configuration
|
||||
default_state = api.data.get("familiesStateDefault", True)
|
||||
toggled = set(api.data.get("familiesStateToggled") or [])
|
||||
|
||||
# Replace icons with a Qt icon we can use in the user interfaces
|
||||
for family in families:
|
||||
name = family["name"]
|
||||
# Set family icon
|
||||
icon = family.get("icon", None)
|
||||
if icon:
|
||||
family["icon"] = qtawesome.icon(
|
||||
"fa.{}".format(icon),
|
||||
color=self.default_color
|
||||
)
|
||||
else:
|
||||
family["icon"] = self.default_icon()
|
||||
|
||||
# Update state
|
||||
if name in toggled:
|
||||
state = True
|
||||
else:
|
||||
state = default_state
|
||||
family["state"] = state
|
||||
|
||||
self.family_configs[name] = family
|
||||
|
||||
return self.family_configs
|
||||
|
||||
|
||||
class GroupsConfig:
|
||||
# Subset group item's default icon and order
|
||||
_default_group_config = None
|
||||
|
||||
def __init__(self, dbcon):
|
||||
self.dbcon = dbcon
|
||||
self.groups = {}
|
||||
|
||||
@classmethod
|
||||
def default_group_config(cls):
|
||||
if cls._default_group_config is None:
|
||||
cls._default_group_config = {
|
||||
"icon": qtawesome.icon(
|
||||
"fa.object-group",
|
||||
color=style.colors.default
|
||||
),
|
||||
"order": 0
|
||||
}
|
||||
return cls._default_group_config
|
||||
|
||||
def refresh(self):
|
||||
"""Get subset group configurations from the database
|
||||
|
||||
The 'group' configuration must be stored in the project `config` field.
|
||||
See schema `config-1.0.json`
|
||||
|
||||
"""
|
||||
# Clear cached groups
|
||||
self.groups.clear()
|
||||
|
||||
group_configs = []
|
||||
project_name = self.dbcon.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
# Get pre-defined group name and apperance from project config
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
projection={"config.groups": True}
|
||||
)
|
||||
|
||||
if project_doc:
|
||||
group_configs = project_doc["config"].get("groups") or []
|
||||
else:
|
||||
print("Project not found! \"{}\"".format(project_name))
|
||||
|
||||
# Build pre-defined group configs
|
||||
for config in group_configs:
|
||||
name = config["name"]
|
||||
icon = "fa." + config.get("icon", "object-group")
|
||||
color = config.get("color", style.colors.default)
|
||||
order = float(config.get("order", 0))
|
||||
|
||||
self.groups[name] = {
|
||||
"icon": qtawesome.icon(icon, color=color),
|
||||
"order": order
|
||||
}
|
||||
|
||||
return self.groups
|
||||
|
||||
def ordered_groups(self, group_names):
|
||||
# default order zero included
|
||||
_orders = set([0])
|
||||
for config in self.groups.values():
|
||||
_orders.add(config["order"])
|
||||
|
||||
# Remap order to list index
|
||||
orders = sorted(_orders)
|
||||
|
||||
_groups = list()
|
||||
for name in group_names:
|
||||
# Get group config
|
||||
config = self.groups.get(name) or self.default_group_config()
|
||||
# Base order
|
||||
remapped_order = orders.index(config["order"])
|
||||
|
||||
data = {
|
||||
"name": name,
|
||||
"icon": config["icon"],
|
||||
"_order": remapped_order,
|
||||
}
|
||||
|
||||
_groups.append(data)
|
||||
|
||||
# Sort by tuple (base_order, name)
|
||||
# If there are multiple groups in same order, will sorted by name.
|
||||
ordered_groups = sorted(
|
||||
_groups, key=lambda _group: (_group.pop("_order"), _group["name"])
|
||||
)
|
||||
|
||||
total = len(ordered_groups)
|
||||
order_temp = "%0{}d".format(len(str(total)))
|
||||
|
||||
# Update sorted order to config
|
||||
for index, group_data in enumerate(ordered_groups):
|
||||
order = index
|
||||
inverse_order = total - index
|
||||
|
||||
# Format orders into fixed length string for groups sorting
|
||||
group_data["order"] = order_temp % order
|
||||
group_data["inverseOrder"] = order_temp % inverse_order
|
||||
|
||||
return ordered_groups
|
||||
|
||||
def active_groups(self, asset_ids, include_predefined=True):
|
||||
"""Collect all active groups from each subset"""
|
||||
# Collect groups from subsets
|
||||
group_names = set(
|
||||
self.dbcon.distinct(
|
||||
"data.subsetGroup",
|
||||
{"type": "subset", "parent": {"$in": asset_ids}}
|
||||
)
|
||||
)
|
||||
if include_predefined:
|
||||
# Ensure all predefined group configs will be included
|
||||
group_names.update(self.groups.keys())
|
||||
|
||||
return self.ordered_groups(group_names)
|
||||
|
||||
def split_subsets_for_groups(self, subset_docs, grouping):
|
||||
"""Collect all active groups from each subset"""
|
||||
subset_docs_without_group = collections.defaultdict(list)
|
||||
subset_docs_by_group = collections.defaultdict(dict)
|
||||
for subset_doc in subset_docs:
|
||||
subset_name = subset_doc["name"]
|
||||
if grouping:
|
||||
group_name = subset_doc["data"].get("subsetGroup")
|
||||
if group_name:
|
||||
if subset_name not in subset_docs_by_group[group_name]:
|
||||
subset_docs_by_group[group_name][subset_name] = []
|
||||
|
||||
subset_docs_by_group[group_name][subset_name].append(
|
||||
subset_doc
|
||||
)
|
||||
continue
|
||||
|
||||
subset_docs_without_group[subset_name].append(subset_doc)
|
||||
|
||||
ordered_groups = self.ordered_groups(subset_docs_by_group.keys())
|
||||
|
||||
return ordered_groups, subset_docs_without_group, subset_docs_by_group
|
||||
|
||||
|
||||
def create_qthread(func, *args, **kwargs):
|
||||
class Thread(QtCore.QThread):
|
||||
def run(self):
|
||||
func(*args, **kwargs)
|
||||
return Thread()
|
||||
|
||||
|
||||
def get_repre_icons():
|
||||
try:
|
||||
from openpype_modules import sync_server
|
||||
except Exception:
|
||||
# Backwards compatibility
|
||||
from openpype.modules import sync_server
|
||||
|
||||
resource_path = os.path.join(
|
||||
os.path.dirname(sync_server.sync_server_module.__file__),
|
||||
"providers", "resources"
|
||||
)
|
||||
icons = {}
|
||||
# TODO get from sync module
|
||||
for provider in ['studio', 'local_drive', 'gdrive']:
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
icons[provider] = QtGui.QIcon(pix_url)
|
||||
|
||||
return icons
|
||||
|
||||
|
||||
def get_progress_for_repre(doc, active_site, remote_site):
|
||||
"""
|
||||
Calculates average progress for representation.
|
||||
|
||||
If site has created_dt >> fully available >> progress == 1
|
||||
|
||||
Could be calculated in aggregate if it would be too slow
|
||||
Args:
|
||||
doc(dict): representation dict
|
||||
Returns:
|
||||
(dict) with active and remote sites progress
|
||||
{'studio': 1.0, 'gdrive': -1} - gdrive site is not present
|
||||
-1 is used to highlight the site should be added
|
||||
{'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not
|
||||
uploaded yet
|
||||
"""
|
||||
progress = {active_site: -1,
|
||||
remote_site: -1}
|
||||
if not doc:
|
||||
return progress
|
||||
|
||||
files = {active_site: 0, remote_site: 0}
|
||||
doc_files = doc.get("files") or []
|
||||
for doc_file in doc_files:
|
||||
if not isinstance(doc_file, dict):
|
||||
continue
|
||||
|
||||
sites = doc_file.get("sites") or []
|
||||
for site in sites:
|
||||
if (
|
||||
# Pype 2 compatibility
|
||||
not isinstance(site, dict)
|
||||
# Check if site name is one of progress sites
|
||||
or site["name"] not in progress
|
||||
):
|
||||
continue
|
||||
|
||||
files[site["name"]] += 1
|
||||
norm_progress = max(progress[site["name"]], 0)
|
||||
if site.get("created_dt"):
|
||||
progress[site["name"]] = norm_progress + 1
|
||||
elif site.get("progress"):
|
||||
progress[site["name"]] = norm_progress + site["progress"]
|
||||
else: # site exists, might be failed, do not add again
|
||||
progress[site["name"]] = 0
|
||||
|
||||
# for example 13 fully avail. files out of 26 >> 13/26 = 0.5
|
||||
avg_progress = {}
|
||||
avg_progress[active_site] = \
|
||||
progress[active_site] / max(files[active_site], 1)
|
||||
avg_progress[remote_site] = \
|
||||
progress[remote_site] / max(files[remote_site], 1)
|
||||
return avg_progress
|
||||
|
||||
|
||||
def is_sync_loader(loader):
|
||||
return is_remove_site_loader(loader) or is_add_site_loader(loader)
|
||||
|
||||
|
||||
def is_remove_site_loader(loader):
|
||||
return hasattr(loader, "remove_site_on_representation")
|
||||
|
||||
|
||||
def is_add_site_loader(loader):
|
||||
return hasattr(loader, "add_site_to_representation")
|
||||
500
openpype/tools/utils/models.py
Normal file
500
openpype/tools/utils/models.py
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
import re
|
||||
import time
|
||||
import logging
|
||||
import collections
|
||||
|
||||
import Qt
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import style, io
|
||||
from . import lib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TreeModel(QtCore.QAbstractItemModel):
|
||||
|
||||
Columns = list()
|
||||
ItemRole = QtCore.Qt.UserRole + 1
|
||||
item_class = None
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
self._root_item = self.ItemClass()
|
||||
|
||||
@property
|
||||
def ItemClass(self):
|
||||
if self.item_class is not None:
|
||||
return self.item_class
|
||||
return Item
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
return parent_item.childCount()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.Columns)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
item = index.internalPointer()
|
||||
column = index.column()
|
||||
|
||||
key = self.Columns[column]
|
||||
return item.get(key, None)
|
||||
|
||||
if role == self.ItemRole:
|
||||
return index.internalPointer()
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
"""Change the data on the items.
|
||||
|
||||
Returns:
|
||||
bool: Whether the edit was successful
|
||||
"""
|
||||
|
||||
if index.isValid():
|
||||
if role == QtCore.Qt.EditRole:
|
||||
|
||||
item = index.internalPointer()
|
||||
column = index.column()
|
||||
key = self.Columns[column]
|
||||
item[key] = value
|
||||
|
||||
# passing `list()` for PyQt5 (see PYSIDE-462)
|
||||
if Qt.__binding__ in ("PyQt4", "PySide"):
|
||||
self.dataChanged.emit(index, index)
|
||||
else:
|
||||
self.dataChanged.emit(index, index, [role])
|
||||
|
||||
# must return true if successful
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def setColumns(self, keys):
|
||||
assert isinstance(keys, (list, tuple))
|
||||
self.Columns = keys
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section < len(self.Columns):
|
||||
return self.Columns[section]
|
||||
|
||||
super(TreeModel, self).headerData(section, orientation, role)
|
||||
|
||||
def flags(self, index):
|
||||
flags = QtCore.Qt.ItemIsEnabled
|
||||
|
||||
item = index.internalPointer()
|
||||
if item.get("enabled", True):
|
||||
flags |= QtCore.Qt.ItemIsSelectable
|
||||
|
||||
return flags
|
||||
|
||||
def parent(self, index):
|
||||
|
||||
item = index.internalPointer()
|
||||
parent_item = item.parent()
|
||||
|
||||
# If it has no parents we return invalid
|
||||
if parent_item == self._root_item or not parent_item:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.createIndex(parent_item.row(), 0, parent_item)
|
||||
|
||||
def index(self, row, column, parent=None):
|
||||
"""Return index for row/column under parent"""
|
||||
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
|
||||
child_item = parent_item.child(row)
|
||||
if child_item:
|
||||
return self.createIndex(row, column, child_item)
|
||||
else:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def add_child(self, item, parent=None):
|
||||
if parent is None:
|
||||
parent = self._root_item
|
||||
|
||||
parent.add_child(item)
|
||||
|
||||
def column_name(self, column):
|
||||
"""Return column key by index"""
|
||||
|
||||
if column < len(self.Columns):
|
||||
return self.Columns[column]
|
||||
|
||||
def clear(self):
|
||||
self.beginResetModel()
|
||||
self._root_item = self.ItemClass()
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class Item(dict):
|
||||
"""An item that can be represented in a tree view using `TreeModel`.
|
||||
|
||||
The item can store data just like a regular dictionary.
|
||||
|
||||
>>> data = {"name": "John", "score": 10}
|
||||
>>> item = Item(data)
|
||||
>>> assert item["name"] == "John"
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data=None):
|
||||
super(Item, self).__init__()
|
||||
|
||||
self._children = list()
|
||||
self._parent = None
|
||||
|
||||
if data is not None:
|
||||
assert isinstance(data, dict)
|
||||
self.update(data)
|
||||
|
||||
def childCount(self):
|
||||
return len(self._children)
|
||||
|
||||
def child(self, row):
|
||||
|
||||
if row >= len(self._children):
|
||||
log.warning("Invalid row as child: {0}".format(row))
|
||||
return
|
||||
|
||||
return self._children[row]
|
||||
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
def row(self):
|
||||
"""
|
||||
Returns:
|
||||
int: Index of this item under parent"""
|
||||
if self._parent is not None:
|
||||
siblings = self.parent().children()
|
||||
return siblings.index(self)
|
||||
return -1
|
||||
|
||||
def add_child(self, child):
|
||||
"""Add a child to this item"""
|
||||
child._parent = self
|
||||
self._children.append(child)
|
||||
|
||||
|
||||
class AssetModel(TreeModel):
|
||||
"""A model listing assets in the silo in the active project.
|
||||
|
||||
The assets are displayed in a treeview, they are visually parented by
|
||||
a `visualParent` field in the database containing an `_id` to a parent
|
||||
asset.
|
||||
|
||||
"""
|
||||
|
||||
Columns = ["label"]
|
||||
Name = 0
|
||||
Deprecated = 2
|
||||
ObjectId = 3
|
||||
|
||||
DocumentRole = QtCore.Qt.UserRole + 2
|
||||
ObjectIdRole = QtCore.Qt.UserRole + 3
|
||||
subsetColorsRole = QtCore.Qt.UserRole + 4
|
||||
|
||||
doc_fetched = QtCore.Signal(bool)
|
||||
refreshed = QtCore.Signal(bool)
|
||||
|
||||
# Asset document projection
|
||||
asset_projection = {
|
||||
"type": 1,
|
||||
"schema": 1,
|
||||
"name": 1,
|
||||
"silo": 1,
|
||||
"data.visualParent": 1,
|
||||
"data.label": 1,
|
||||
"data.tags": 1,
|
||||
"data.icon": 1,
|
||||
"data.color": 1,
|
||||
"data.deprecated": 1
|
||||
}
|
||||
|
||||
def __init__(self, dbcon=None, parent=None, asset_projection=None):
|
||||
super(AssetModel, self).__init__(parent=parent)
|
||||
if dbcon is None:
|
||||
dbcon = io
|
||||
self.dbcon = dbcon
|
||||
self.asset_colors = {}
|
||||
|
||||
# Projections for Mongo queries
|
||||
# - let ability to modify them if used in tools that require more than
|
||||
# defaults
|
||||
if asset_projection:
|
||||
self.asset_projection = asset_projection
|
||||
|
||||
self.asset_projection = asset_projection
|
||||
|
||||
self._doc_fetching_thread = None
|
||||
self._doc_fetching_stop = False
|
||||
self._doc_payload = {}
|
||||
|
||||
self.doc_fetched.connect(self.on_doc_fetched)
|
||||
|
||||
self.refresh()
|
||||
|
||||
def _add_hierarchy(self, assets, parent=None, silos=None):
|
||||
"""Add the assets that are related to the parent as children items.
|
||||
|
||||
This method does *not* query the database. These instead are queried
|
||||
in a single batch upfront as an optimization to reduce database
|
||||
queries. Resulting in up to 10x speed increase.
|
||||
|
||||
Args:
|
||||
assets (dict): All assets in the currently active silo stored
|
||||
by key/value
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Reset colors
|
||||
self.asset_colors = {}
|
||||
|
||||
if silos:
|
||||
# WARNING: Silo item "_id" is set to silo value
|
||||
# mainly because GUI issue with perserve selection and expanded row
|
||||
# and because of easier hierarchy parenting (in "assets")
|
||||
for silo in silos:
|
||||
item = Item({
|
||||
"_id": silo,
|
||||
"name": silo,
|
||||
"label": silo,
|
||||
"type": "silo"
|
||||
})
|
||||
self.add_child(item, parent=parent)
|
||||
self._add_hierarchy(assets, parent=item)
|
||||
|
||||
parent_id = parent["_id"] if parent else None
|
||||
current_assets = assets.get(parent_id, list())
|
||||
|
||||
for asset in current_assets:
|
||||
# get label from data, otherwise use name
|
||||
data = asset.get("data", {})
|
||||
label = data.get("label", asset["name"])
|
||||
tags = data.get("tags", [])
|
||||
|
||||
# store for the asset for optimization
|
||||
deprecated = "deprecated" in tags
|
||||
|
||||
item = Item({
|
||||
"_id": asset["_id"],
|
||||
"name": asset["name"],
|
||||
"label": label,
|
||||
"type": asset["type"],
|
||||
"tags": ", ".join(tags),
|
||||
"deprecated": deprecated,
|
||||
"_document": asset
|
||||
})
|
||||
self.add_child(item, parent=parent)
|
||||
|
||||
# Add asset's children recursively if it has children
|
||||
if asset["_id"] in assets:
|
||||
self._add_hierarchy(assets, parent=item)
|
||||
|
||||
self.asset_colors[asset["_id"]] = []
|
||||
|
||||
def on_doc_fetched(self, was_stopped):
|
||||
if was_stopped:
|
||||
self.stop_fetch_thread()
|
||||
return
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
assets_by_parent = self._doc_payload.get("assets_by_parent")
|
||||
silos = self._doc_payload.get("silos")
|
||||
if assets_by_parent is not None:
|
||||
# Build the hierarchical tree items recursively
|
||||
self._add_hierarchy(
|
||||
assets_by_parent,
|
||||
parent=None,
|
||||
silos=silos
|
||||
)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
has_content = bool(assets_by_parent) or bool(silos)
|
||||
self.refreshed.emit(has_content)
|
||||
|
||||
self.stop_fetch_thread()
|
||||
|
||||
def fetch(self):
|
||||
self._doc_payload = self._fetch() or {}
|
||||
# Emit doc fetched only if was not stopped
|
||||
self.doc_fetched.emit(self._doc_fetching_stop)
|
||||
|
||||
def _fetch(self):
|
||||
if not self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
return
|
||||
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
{"_id": True}
|
||||
)
|
||||
if not project_doc:
|
||||
return
|
||||
|
||||
# Get all assets sorted by name
|
||||
db_assets = self.dbcon.find(
|
||||
{"type": "asset"},
|
||||
self.asset_projection
|
||||
).sort("name", 1)
|
||||
|
||||
# Group the assets by their visual parent's id
|
||||
assets_by_parent = collections.defaultdict(list)
|
||||
for asset in db_assets:
|
||||
if self._doc_fetching_stop:
|
||||
return
|
||||
parent_id = asset.get("data", {}).get("visualParent")
|
||||
assets_by_parent[parent_id].append(asset)
|
||||
|
||||
return {
|
||||
"assets_by_parent": assets_by_parent,
|
||||
"silos": None
|
||||
}
|
||||
|
||||
def stop_fetch_thread(self):
|
||||
if self._doc_fetching_thread is not None:
|
||||
self._doc_fetching_stop = True
|
||||
while self._doc_fetching_thread.isRunning():
|
||||
time.sleep(0.001)
|
||||
self._doc_fetching_thread = None
|
||||
|
||||
def refresh(self, force=False):
|
||||
"""Refresh the data for the model."""
|
||||
# Skip fetch if there is already other thread fetching documents
|
||||
if self._doc_fetching_thread is not None:
|
||||
if not force:
|
||||
return
|
||||
self.stop_fetch_thread()
|
||||
|
||||
# Clear model items
|
||||
self.clear()
|
||||
|
||||
# Fetch documents from mongo
|
||||
# Restart payload
|
||||
self._doc_payload = {}
|
||||
self._doc_fetching_stop = False
|
||||
self._doc_fetching_thread = lib.create_qthread(self.fetch)
|
||||
self._doc_fetching_thread.start()
|
||||
|
||||
def flags(self, index):
|
||||
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
if role == self.subsetColorsRole:
|
||||
asset_id = index.data(self.ObjectIdRole)
|
||||
self.asset_colors[asset_id] = value
|
||||
|
||||
if Qt.__binding__ in ("PyQt4", "PySide"):
|
||||
self.dataChanged.emit(index, index)
|
||||
else:
|
||||
self.dataChanged.emit(index, index, [role])
|
||||
|
||||
return True
|
||||
|
||||
return super(AssetModel, self).setData(index, value, role)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item = index.internalPointer()
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
column = index.column()
|
||||
if column == self.Name:
|
||||
# Allow a custom icon and custom icon color to be defined
|
||||
data = item.get("_document", {}).get("data", {})
|
||||
icon = data.get("icon", None)
|
||||
if icon is None and item.get("type") == "silo":
|
||||
icon = "database"
|
||||
color = data.get("color", style.colors.default)
|
||||
|
||||
if icon is None:
|
||||
# Use default icons if no custom one is specified.
|
||||
# If it has children show a full folder, otherwise
|
||||
# show an open folder
|
||||
has_children = self.rowCount(index) > 0
|
||||
icon = "folder" if has_children else "folder-o"
|
||||
|
||||
# Make the color darker when the asset is deprecated
|
||||
if item.get("deprecated", False):
|
||||
color = QtGui.QColor(color).darker(250)
|
||||
|
||||
try:
|
||||
key = "fa.{0}".format(icon) # font-awesome key
|
||||
icon = qtawesome.icon(key, color=color)
|
||||
return icon
|
||||
except Exception as exception:
|
||||
# Log an error message instead of erroring out completely
|
||||
# when the icon couldn't be created (e.g. invalid name)
|
||||
log.error(exception)
|
||||
|
||||
return
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole: # font color
|
||||
if "deprecated" in item.get("tags", []):
|
||||
return QtGui.QColor(style.colors.light).darker(250)
|
||||
|
||||
if role == self.ObjectIdRole:
|
||||
return item.get("_id", None)
|
||||
|
||||
if role == self.DocumentRole:
|
||||
return item.get("_document", None)
|
||||
|
||||
if role == self.subsetColorsRole:
|
||||
asset_id = item.get("_id", None)
|
||||
return self.asset_colors.get(asset_id) or []
|
||||
|
||||
return super(AssetModel, self).data(index, role)
|
||||
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
pattern = regex.pattern()
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
if source_index.isValid():
|
||||
# Check current index itself
|
||||
key = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for i in range(rows):
|
||||
if self.filterAcceptsRow(i, source_index):
|
||||
return True
|
||||
|
||||
# Otherwise filter it
|
||||
return False
|
||||
|
||||
return super(
|
||||
RecursiveSortFilterProxyModel, self
|
||||
).filterAcceptsRow(row, parent)
|
||||
86
openpype/tools/utils/views.py
Normal file
86
openpype/tools/utils/views.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import os
|
||||
from avalon import style
|
||||
from Qt import QtWidgets, QtCore, QtGui, QtSvg
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
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())
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class TreeViewSpinner(QtWidgets.QTreeView):
|
||||
size = 160
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeViewSpinner, self).__init__(parent=parent)
|
||||
|
||||
loading_image_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(style.__file__)),
|
||||
"svg",
|
||||
"spinner-200.svg"
|
||||
)
|
||||
self.spinner = QtSvg.QSvgRenderer(loading_image_path)
|
||||
|
||||
self.is_loading = False
|
||||
self.is_empty = True
|
||||
|
||||
def paint_loading(self, event):
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
rect.moveTo(
|
||||
rect.x() + rect.width() / 2 - self.size / 2,
|
||||
rect.y() + rect.height() / 2 - self.size / 2
|
||||
)
|
||||
rect.setSize(QtCore.QSizeF(self.size, self.size))
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
self.spinner.render(painter, rect)
|
||||
|
||||
def paint_empty(self, event):
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
qtext_opt = QtGui.QTextOption(
|
||||
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
painter.drawText(rect, "No Data", qtext_opt)
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self.is_loading:
|
||||
self.paint_loading(event)
|
||||
elif self.is_empty:
|
||||
self.paint_empty(event)
|
||||
else:
|
||||
super(TreeViewSpinner, self).paintEvent(event)
|
||||
|
||||
|
||||
class AssetsView(TreeViewSpinner, DeselectableTreeView):
|
||||
"""Item view.
|
||||
This implements a context menu.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(AssetsView, self).__init__()
|
||||
self.setIndentation(15)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.setHeaderHidden(True)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
if modifiers == QtCore.Qt.ShiftModifier:
|
||||
return
|
||||
elif modifiers == QtCore.Qt.ControlModifier:
|
||||
return
|
||||
|
||||
super(AssetsView, self).mousePressEvent(event)
|
||||
499
openpype/tools/utils/widgets.py
Normal file
499
openpype/tools/utils/widgets.py
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
from . import lib
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from avalon.vendor import qtawesome, qargparse
|
||||
|
||||
from avalon import style
|
||||
|
||||
from .models import AssetModel, RecursiveSortFilterProxyModel
|
||||
from .views import AssetsView
|
||||
from .delegates import AssetDelegate
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssetWidget(QtWidgets.QWidget):
|
||||
"""A Widget to display a tree of assets with filter
|
||||
|
||||
To list the assets of the active project:
|
||||
>>> # widget = AssetWidget()
|
||||
>>> # widget.refresh()
|
||||
>>> # widget.show()
|
||||
|
||||
"""
|
||||
|
||||
refresh_triggered = QtCore.Signal() # on model refresh
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal() # on view selection change
|
||||
current_changed = QtCore.Signal() # on view current index change
|
||||
|
||||
def __init__(self, dbcon, multiselection=False, parent=None):
|
||||
super(AssetWidget, self).__init__(parent=parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Tree View
|
||||
model = AssetModel(dbcon=self.dbcon, parent=self)
|
||||
proxy = RecursiveSortFilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
view = AssetsView()
|
||||
view.setModel(proxy)
|
||||
if multiselection:
|
||||
asset_delegate = AssetDelegate()
|
||||
view.setSelectionMode(view.ExtendedSelection)
|
||||
view.setItemDelegate(asset_delegate)
|
||||
|
||||
# Header
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
|
||||
icon = qtawesome.icon("fa.arrow-down", color=style.colors.light)
|
||||
set_current_asset_btn = QtWidgets.QPushButton(icon, "")
|
||||
set_current_asset_btn.setToolTip("Go to Asset from current Session")
|
||||
# Hide by default
|
||||
set_current_asset_btn.setVisible(False)
|
||||
|
||||
icon = qtawesome.icon("fa.refresh", color=style.colors.light)
|
||||
refresh = QtWidgets.QPushButton(icon, "")
|
||||
refresh.setToolTip("Refresh items")
|
||||
|
||||
filter = QtWidgets.QLineEdit()
|
||||
filter.textChanged.connect(proxy.setFilterFixedString)
|
||||
filter.setPlaceholderText("Filter assets..")
|
||||
|
||||
header.addWidget(filter)
|
||||
header.addWidget(set_current_asset_btn)
|
||||
header.addWidget(refresh)
|
||||
|
||||
# Layout
|
||||
layout.addLayout(header)
|
||||
layout.addWidget(view)
|
||||
|
||||
# Signals/Slots
|
||||
selection = view.selectionModel()
|
||||
selection.selectionChanged.connect(self.selection_changed)
|
||||
selection.currentChanged.connect(self.current_changed)
|
||||
refresh.clicked.connect(self.refresh)
|
||||
set_current_asset_btn.clicked.connect(self.set_current_session_asset)
|
||||
|
||||
self.set_current_asset_btn = set_current_asset_btn
|
||||
self.model = model
|
||||
self.proxy = proxy
|
||||
self.view = view
|
||||
|
||||
self.model_selection = {}
|
||||
|
||||
def set_current_asset_btn_visibility(self, visible=None):
|
||||
"""Hide set current asset button.
|
||||
|
||||
Not all tools support using of current context asset.
|
||||
"""
|
||||
if visible is None:
|
||||
visible = not self.set_current_asset_btn.isVisible()
|
||||
self.set_current_asset_btn.setVisible(visible)
|
||||
|
||||
def _refresh_model(self):
|
||||
# Store selection
|
||||
self._store_model_selection()
|
||||
time_start = time.time()
|
||||
|
||||
self.set_loading_state(
|
||||
loading=True,
|
||||
empty=True
|
||||
)
|
||||
|
||||
def on_refreshed(has_item):
|
||||
self.set_loading_state(loading=False, empty=not has_item)
|
||||
self._restore_model_selection()
|
||||
self.model.refreshed.disconnect()
|
||||
self.refreshed.emit()
|
||||
print("Duration: %.3fs" % (time.time() - time_start))
|
||||
|
||||
# Connect to signal
|
||||
self.model.refreshed.connect(on_refreshed)
|
||||
# Trigger signal before refresh is called
|
||||
self.refresh_triggered.emit()
|
||||
# Refresh model
|
||||
self.model.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_model()
|
||||
|
||||
def get_active_asset(self):
|
||||
"""Return the asset item of the current selection."""
|
||||
current = self.view.currentIndex()
|
||||
return current.data(self.model.ItemRole)
|
||||
|
||||
def get_active_asset_document(self):
|
||||
"""Return the asset document of the current selection."""
|
||||
current = self.view.currentIndex()
|
||||
return current.data(self.model.DocumentRole)
|
||||
|
||||
def get_active_index(self):
|
||||
return self.view.currentIndex()
|
||||
|
||||
def get_selected_assets(self):
|
||||
"""Return the documents of selected assets."""
|
||||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows()
|
||||
assets = [row.data(self.model.DocumentRole) for row in rows]
|
||||
|
||||
# NOTE: skip None object assumed they are silo (backwards comp.)
|
||||
return [asset for asset in assets if asset]
|
||||
|
||||
def select_assets(self, assets, expand=True, key="name"):
|
||||
"""Select assets by item key.
|
||||
|
||||
Args:
|
||||
assets (list): List of asset values that can be found under
|
||||
specified `key`
|
||||
expand (bool): Whether to also expand to the asset in the view
|
||||
key (string): Key that specifies where to look for `assets` values
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Default `key` is "name" in that case `assets` should contain single
|
||||
asset name or list of asset names. (It is good idea to use "_id" key
|
||||
instead of name in that case `assets` must contain `ObjectId` object/s)
|
||||
It is expected that each value in `assets` will be found only once.
|
||||
If the filters according to the `key` and `assets` correspond to
|
||||
the more asset, only the first found will be selected.
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(assets, (tuple, list)):
|
||||
assets = [assets]
|
||||
|
||||
# convert to list - tuple cant be modified
|
||||
assets = set(assets)
|
||||
|
||||
# Clear selection
|
||||
selection_model = self.view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
# Select
|
||||
mode = selection_model.Select | selection_model.Rows
|
||||
for index in lib.iter_model_rows(
|
||||
self.proxy, column=0, include_root=False
|
||||
):
|
||||
# stop iteration if there are no assets to process
|
||||
if not assets:
|
||||
break
|
||||
|
||||
value = index.data(self.model.ItemRole).get(key)
|
||||
if value not in assets:
|
||||
continue
|
||||
|
||||
# Remove processed asset
|
||||
assets.discard(value)
|
||||
|
||||
selection_model.select(index, mode)
|
||||
if expand:
|
||||
# Expand parent index
|
||||
self.view.expand(self.proxy.parent(index))
|
||||
|
||||
# Set the currently active index
|
||||
self.view.setCurrentIndex(index)
|
||||
|
||||
def set_loading_state(self, loading, empty):
|
||||
if self.view.is_loading != loading:
|
||||
if loading:
|
||||
self.view.spinner.repaintNeeded.connect(
|
||||
self.view.viewport().update
|
||||
)
|
||||
else:
|
||||
self.view.spinner.repaintNeeded.disconnect()
|
||||
|
||||
self.view.is_loading = loading
|
||||
self.view.is_empty = empty
|
||||
|
||||
def _store_model_selection(self):
|
||||
index = self.view.currentIndex()
|
||||
current = None
|
||||
if index and index.isValid():
|
||||
current = index.data(self.model.ObjectIdRole)
|
||||
|
||||
expanded = set()
|
||||
model = self.view.model()
|
||||
for index in lib.iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
if self.view.isExpanded(index):
|
||||
value = index.data(self.model.ObjectIdRole)
|
||||
expanded.add(value)
|
||||
|
||||
selection_model = self.view.selectionModel()
|
||||
|
||||
selected = None
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if selected_rows:
|
||||
selected = set(
|
||||
row.data(self.model.ObjectIdRole)
|
||||
for row in selected_rows
|
||||
)
|
||||
|
||||
self.model_selection = {
|
||||
"expanded": expanded,
|
||||
"selected": selected,
|
||||
"current": current
|
||||
}
|
||||
|
||||
def _restore_model_selection(self):
|
||||
model = self.view.model()
|
||||
not_set = object()
|
||||
expanded = self.model_selection.pop("expanded", not_set)
|
||||
selected = self.model_selection.pop("selected", not_set)
|
||||
current = self.model_selection.pop("current", not_set)
|
||||
|
||||
if (
|
||||
expanded is not_set
|
||||
or selected is not_set
|
||||
or current is not_set
|
||||
):
|
||||
return
|
||||
|
||||
if expanded:
|
||||
for index in lib.iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
is_expanded = index.data(self.model.ObjectIdRole) in expanded
|
||||
self.view.setExpanded(index, is_expanded)
|
||||
|
||||
if not selected and not current:
|
||||
self.set_current_session_asset()
|
||||
return
|
||||
|
||||
current_index = None
|
||||
selected_indexes = []
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in lib.iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
object_id = index.data(self.model.ObjectIdRole)
|
||||
if object_id in selected:
|
||||
selected_indexes.append(index)
|
||||
|
||||
if not current_index and object_id == current:
|
||||
current_index = index
|
||||
|
||||
if current_index:
|
||||
self.view.setCurrentIndex(current_index)
|
||||
|
||||
if not selected_indexes:
|
||||
return
|
||||
selection_model = self.view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
for index in selected_indexes:
|
||||
# Ensure item is visible
|
||||
self.view.scrollTo(index)
|
||||
selection_model.select(index, flags)
|
||||
|
||||
def set_current_session_asset(self):
|
||||
asset_name = self.dbcon.Session.get("AVALON_ASSET")
|
||||
if asset_name:
|
||||
self.select_assets([asset_name])
|
||||
|
||||
|
||||
class OptionalMenu(QtWidgets.QMenu):
|
||||
"""A subclass of `QtWidgets.QMenu` to work with `OptionalAction`
|
||||
|
||||
This menu has reimplemented `mouseReleaseEvent`, `mouseMoveEvent` and
|
||||
`leaveEvent` to provide better action hightlighting and triggering for
|
||||
actions that were instances of `QtWidgets.QWidgetAction`.
|
||||
|
||||
"""
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Emit option clicked signal if mouse released on it"""
|
||||
active = self.actionAt(event.pos())
|
||||
if active and active.use_option:
|
||||
option = active.widget.option
|
||||
if option.is_hovered(event.globalPos()):
|
||||
option.clicked.emit()
|
||||
super(OptionalMenu, self).mouseReleaseEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Add highlight to active action"""
|
||||
active = self.actionAt(event.pos())
|
||||
for action in self.actions():
|
||||
action.set_highlight(action is active, event.globalPos())
|
||||
super(OptionalMenu, self).mouseMoveEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
"""Remove highlight from all actions"""
|
||||
for action in self.actions():
|
||||
action.set_highlight(False)
|
||||
super(OptionalMenu, self).leaveEvent(event)
|
||||
|
||||
|
||||
class OptionalAction(QtWidgets.QWidgetAction):
|
||||
"""Menu action with option box
|
||||
|
||||
A menu action like Maya's menu item with option box, implemented by
|
||||
subclassing `QtWidgets.QWidgetAction`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label, icon, use_option, parent):
|
||||
super(OptionalAction, self).__init__(parent)
|
||||
self.label = label
|
||||
self.icon = icon
|
||||
self.use_option = use_option
|
||||
self.option_tip = ""
|
||||
self.optioned = False
|
||||
|
||||
def createWidget(self, parent):
|
||||
widget = OptionalActionWidget(self.label, parent)
|
||||
self.widget = widget
|
||||
|
||||
if self.icon:
|
||||
widget.setIcon(self.icon)
|
||||
|
||||
if self.use_option:
|
||||
widget.option.clicked.connect(self.on_option)
|
||||
widget.option.setToolTip(self.option_tip)
|
||||
else:
|
||||
widget.option.setVisible(False)
|
||||
|
||||
return widget
|
||||
|
||||
def set_option_tip(self, options):
|
||||
sep = "\n\n"
|
||||
mak = (lambda opt: opt["name"] + " :\n " + opt["help"])
|
||||
self.option_tip = sep.join(mak(opt) for opt in options)
|
||||
|
||||
def on_option(self):
|
||||
self.optioned = True
|
||||
|
||||
def set_highlight(self, state, global_pos=None):
|
||||
body = self.widget.body
|
||||
option = self.widget.option
|
||||
|
||||
role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window
|
||||
body.setBackgroundRole(role)
|
||||
body.setAutoFillBackground(state)
|
||||
|
||||
if not self.use_option:
|
||||
return
|
||||
|
||||
state = option.is_hovered(global_pos)
|
||||
role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window
|
||||
option.setBackgroundRole(role)
|
||||
option.setAutoFillBackground(state)
|
||||
|
||||
|
||||
class OptionalActionWidget(QtWidgets.QWidget):
|
||||
"""Main widget class for `OptionalAction`"""
|
||||
|
||||
def __init__(self, label, parent=None):
|
||||
super(OptionalActionWidget, self).__init__(parent)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
body.setStyleSheet("background: transparent;")
|
||||
|
||||
icon = QtWidgets.QLabel()
|
||||
label = QtWidgets.QLabel(label)
|
||||
option = OptionBox(body)
|
||||
|
||||
icon.setFixedSize(24, 16)
|
||||
option.setFixedSize(30, 30)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(body)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(2)
|
||||
layout.addWidget(icon)
|
||||
layout.addWidget(label)
|
||||
layout.addSpacing(6)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(6, 1, 2, 1)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(option)
|
||||
|
||||
body.setMouseTracking(True)
|
||||
label.setMouseTracking(True)
|
||||
option.setMouseTracking(True)
|
||||
self.setMouseTracking(True)
|
||||
self.setFixedHeight(32)
|
||||
|
||||
self.icon = icon
|
||||
self.label = label
|
||||
self.option = option
|
||||
self.body = body
|
||||
|
||||
# (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke.
|
||||
# See https://stackoverflow.com/q/52838690/4145300
|
||||
label.setStyle(QtWidgets.QStyleFactory.create("Plastique"))
|
||||
|
||||
def setIcon(self, icon):
|
||||
pixmap = icon.pixmap(16, 16)
|
||||
self.icon.setPixmap(pixmap)
|
||||
|
||||
|
||||
class OptionBox(QtWidgets.QLabel):
|
||||
"""Option box widget class for `OptionalActionWidget`"""
|
||||
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(OptionBox, self).__init__(parent)
|
||||
|
||||
self.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
icon = qtawesome.icon("fa.sticky-note-o", color="#c6c6c6")
|
||||
pixmap = icon.pixmap(18, 18)
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
self.setStyleSheet("background: transparent;")
|
||||
|
||||
def is_hovered(self, global_pos):
|
||||
if global_pos is None:
|
||||
return False
|
||||
pos = self.mapFromGlobal(global_pos)
|
||||
return self.rect().contains(pos)
|
||||
|
||||
|
||||
class OptionDialog(QtWidgets.QDialog):
|
||||
"""Option dialog shown by option box"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(OptionDialog, self).__init__(parent)
|
||||
self.setModal(True)
|
||||
self._options = dict()
|
||||
|
||||
def create(self, options):
|
||||
parser = qargparse.QArgumentParser(arguments=options)
|
||||
|
||||
decision = QtWidgets.QWidget()
|
||||
accept = QtWidgets.QPushButton("Accept")
|
||||
cancel = QtWidgets.QPushButton("Cancel")
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(decision)
|
||||
layout.addWidget(accept)
|
||||
layout.addWidget(cancel)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(parser)
|
||||
layout.addWidget(decision)
|
||||
|
||||
accept.clicked.connect(self.accept)
|
||||
cancel.clicked.connect(self.reject)
|
||||
parser.changed.connect(self.on_changed)
|
||||
|
||||
def on_changed(self, argument):
|
||||
self._options[argument["name"]] = argument.read()
|
||||
|
||||
def parse(self):
|
||||
return self._options.copy()
|
||||
|
|
@ -3,7 +3,31 @@ import sys
|
|||
from semver import VersionInfo
|
||||
from git import Repo
|
||||
from optparse import OptionParser
|
||||
from github import Github
|
||||
import os
|
||||
|
||||
def get_release_type_github(Log, github_token):
|
||||
# print(Log)
|
||||
minor_labels = ["type: feature", "type: deprecated"]
|
||||
patch_labels = ["type: enhancement", "type: bug"]
|
||||
|
||||
g = Github(github_token)
|
||||
repo = g.get_repo("pypeclub/OpenPype")
|
||||
|
||||
for line in Log.splitlines():
|
||||
print(line)
|
||||
match = re.search("pull request #(\d+)", line)
|
||||
if match:
|
||||
pr_number = match.group(1)
|
||||
pr = repo.get_pull(int(pr_number))
|
||||
for label in pr.labels:
|
||||
print(label.name)
|
||||
if label.name in minor_labels:
|
||||
return ("minor")
|
||||
elif label.name in patch_labels:
|
||||
return("patch")
|
||||
return None
|
||||
|
||||
|
||||
def remove_prefix(text, prefix):
|
||||
return text[text.startswith(prefix) and len(prefix):]
|
||||
|
|
@ -36,7 +60,7 @@ def get_log_since_tag(version):
|
|||
|
||||
def release_type(log):
|
||||
regex_minor = ["feature/", "(feat)"]
|
||||
regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/"]
|
||||
regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/", "update"]
|
||||
for reg in regex_minor:
|
||||
if re.search(reg, log):
|
||||
return "minor"
|
||||
|
|
@ -135,17 +159,23 @@ def main():
|
|||
parser.add_option("-l", "--lastversion",
|
||||
dest="lastversion", action="store",
|
||||
help="work with explicit version")
|
||||
parser.add_option("-g", "--github_token",
|
||||
dest="github_token", action="store",
|
||||
help="github token")
|
||||
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.bump:
|
||||
last_CI, last_CI_tag = get_last_version("CI")
|
||||
last_release, last_release_tag = get_last_version("release")
|
||||
bump_type_CI = release_type(get_log_since_tag(last_CI_tag))
|
||||
bump_type_release = release_type(get_log_since_tag(last_release_tag))
|
||||
if bump_type_CI is None or bump_type_release is None:
|
||||
bump_type_release = get_release_type_github(
|
||||
get_log_since_tag(last_release_tag),
|
||||
options.github_token
|
||||
)
|
||||
if bump_type_release is None:
|
||||
print("skip")
|
||||
else:
|
||||
print(bump_type_release)
|
||||
|
||||
if options.nightly:
|
||||
next_tag_v = calculate_next_nightly()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue