removed unused assets_widget.py

This commit is contained in:
Jakub Trllo 2024-03-04 17:49:52 +01:00
parent 016056e3df
commit e8a5230fc7

View file

@ -1,694 +0,0 @@
import time
import collections
from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
import ayon_api
from ayon_core.client import get_assets
from ayon_core.style import (
get_default_tools_icon_color,
get_default_entity_icon_color,
)
from ayon_core.tools.flickcharm import FlickCharm
from .views import (
TreeViewSpinner,
DeselectableTreeView
)
from .widgets import PlaceholderLineEdit
from .models import RecursiveSortFilterProxyModel
from .lib import (
DynamicQThread,
get_qta_icon_by_name_and_color
)
ASSET_ID_ROLE = QtCore.Qt.UserRole + 1
ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2
ASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3
ASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4
ASSET_PATH_ROLE = QtCore.Qt.UserRole + 5
def _get_default_asset_icon_name(has_children):
if has_children:
return "fa.folder"
return "fa.folder-o"
def _get_asset_icon_color_from_doc(asset_doc):
if asset_doc:
return asset_doc["data"].get("color")
return None
def _get_asset_icon_name_from_doc(asset_doc):
if asset_doc:
return asset_doc["data"].get("icon")
return None
def _get_asset_icon_color(asset_doc):
icon_color = _get_asset_icon_color_from_doc(asset_doc)
if icon_color:
return icon_color
return get_default_entity_icon_color()
def _get_asset_icon_name(asset_doc, has_children=True):
icon_name = _get_asset_icon_name_from_doc(asset_doc)
if icon_name:
return icon_name
return _get_default_asset_icon_name(has_children)
def get_asset_icon(asset_doc, has_children=False):
"""Get asset icon.
Deprecated:
This function will be removed in future releases. Use on your own
risk.
Args:
asset_doc (dict): Asset document.
has_children (Optional[bool]): Asset has children assets.
Returns:
QIcon: Asset icon.
"""
icon_name = _get_asset_icon_name(asset_doc, has_children)
icon_color = _get_asset_icon_color(asset_doc)
return get_qta_icon_by_name_and_color(icon_name, icon_color)
class _AssetsView(TreeViewSpinner, DeselectableTreeView):
"""Asset items view.
Adds abilities to deselect, show loading spinner and add flick charm
(scroll by mouse/touchpad click and move).
"""
def __init__(self, parent=None):
super(_AssetsView, self).__init__(parent)
self.setIndentation(15)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setHeaderHidden(True)
self._flick_charm_activated = False
self._flick_charm = FlickCharm(parent=self)
self._before_flick_scroll_mode = None
def activate_flick_charm(self):
if self._flick_charm_activated:
return
self._flick_charm_activated = True
self._before_flick_scroll_mode = self.verticalScrollMode()
self._flick_charm.activateOn(self)
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
def deactivate_flick_charm(self):
if not self._flick_charm_activated:
return
self._flick_charm_activated = False
self._flick_charm.deactivateFrom(self)
if self._before_flick_scroll_mode is not None:
self.setVerticalScrollMode(self._before_flick_scroll_mode)
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)
def set_loading_state(self, loading, empty):
"""Change loading state.
TODO: Separate into 2 individual methods.
Args:
loading(bool): Is loading.
empty(bool): Is model empty.
"""
if self.is_loading != loading:
if loading:
self.spinner.repaintNeeded.connect(
self.viewport().update
)
else:
self.spinner.repaintNeeded.disconnect()
self.viewport().update()
self.is_loading = loading
self.is_empty = empty
class _AssetModel(QtGui.QStandardItemModel):
"""A model listing assets 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.
Asset document may have defined label, icon or icon color.
Loading of data for model happens in thread which means that refresh
is not sequential. When refresh is triggered it is required to listen for
'refreshed' signal.
Args:
parent (QObject): Parent Qt object.
"""
_doc_fetched = QtCore.Signal()
refreshed = QtCore.Signal(bool)
# Asset document projection
_asset_projection = {
"name": 1,
"parent": 1,
"data.visualParent": 1,
"data.label": 1,
"data.icon": 1,
"data.color": 1
}
def __init__(self, parent=None):
super(_AssetModel, self).__init__(parent=parent)
self._refreshing = False
self._doc_fetching_thread = None
self._doc_fetching_stop = False
self._doc_payload = []
self._doc_fetched.connect(self._on_docs_fetched)
self._item_ids_with_color = set()
self._items_by_asset_id = {}
self._project_name = None
self._last_project_name = None
@property
def refreshing(self):
return self._refreshing
def get_index_by_asset_id(self, asset_id):
item = self._items_by_asset_id.get(asset_id)
if item is not None:
return item.index()
return QtCore.QModelIndex()
def get_indexes_by_asset_ids(self, asset_ids):
return [
self.get_index_by_asset_id(asset_id)
for asset_id in asset_ids
]
def get_index_by_asset_name(self, asset_name):
indexes = self.get_indexes_by_asset_names([asset_name])
for index in indexes:
if index.isValid():
return index
return indexes[0]
def get_indexes_by_asset_names(self, asset_names):
asset_ids_by_name = {
asset_name: None
for asset_name in asset_names
}
for asset_id, item in self._items_by_asset_id.items():
asset_name = item.data(ASSET_NAME_ROLE)
if asset_name in asset_ids_by_name:
asset_ids_by_name[asset_name] = asset_id
asset_ids = [
asset_ids_by_name[asset_name]
for asset_name in asset_names
]
return self.get_indexes_by_asset_ids(asset_ids)
def get_project_name(self):
return self._project_name
def set_project_name(self, project_name, refresh):
if self._project_name == project_name:
return
self._project_name = project_name
if refresh:
self.refresh()
def refresh(self, force=False):
"""Refresh the data for the model.
Args:
force (bool): Stop currently running refresh start new refresh.
"""
# Skip fetch if there is already other thread fetching documents
if self._refreshing:
if not force:
return
self.stop_refresh()
project_name = self._project_name
clear_model = False
if project_name != self._last_project_name:
clear_model = True
self._last_project_name = project_name
if clear_model:
self._clear_items()
# Fetch documents from mongo
# Restart payload
self._refreshing = True
self._doc_payload = []
self._doc_fetching_thread = DynamicQThread(self._threaded_fetch)
self._doc_fetching_thread.start()
def stop_refresh(self):
self._stop_fetch_thread()
def _clear_items(self):
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
self._items_by_asset_id = {}
self._item_ids_with_color = set()
def _on_docs_fetched(self):
# Make sure refreshing did not change
# - since this line is refreshing sequential and
# triggering of new refresh will happen when this method is done
if not self._refreshing:
self._clear_items()
return
self._fill_assets(self._doc_payload)
self.refreshed.emit(bool(self._items_by_asset_id))
self._stop_fetch_thread()
def _fill_assets(self, asset_docs):
# Collect asset documents as needed
asset_ids = set()
asset_docs_by_id = {}
asset_ids_by_parents = collections.defaultdict(set)
for asset_doc in asset_docs:
asset_id = asset_doc["_id"]
asset_data = asset_doc.get("data") or {}
parent_id = asset_data.get("visualParent")
asset_ids.add(asset_id)
asset_docs_by_id[asset_id] = asset_doc
asset_ids_by_parents[parent_id].add(asset_id)
# Prepare removed asset ids
removed_asset_ids = (
set(self._items_by_asset_id.keys()) - set(asset_docs_by_id.keys())
)
# Prepare queue for adding new items
asset_items_queue = collections.deque()
# Queue starts with root item and 'visualParent' None
root_item = self.invisibleRootItem()
asset_items_queue.append((None, root_item))
while asset_items_queue:
# Get item from queue
parent_id, parent_item = asset_items_queue.popleft()
# Skip if there are no children
children_ids = asset_ids_by_parents[parent_id]
# Go through current children of parent item
# - find out items that were deleted and skip creation of already
# existing items
for row in reversed(range(parent_item.rowCount())):
child_item = parent_item.child(row, 0)
asset_id = child_item.data(ASSET_ID_ROLE)
# Remove item that is not available
if asset_id not in children_ids:
if asset_id in removed_asset_ids:
# Remove and destroy row
parent_item.removeRow(row)
else:
# Just take the row from parent without destroying
parent_item.takeRow(row)
continue
# Remove asset id from `children_ids` set
# - is used as set for creation of "new items"
children_ids.remove(asset_id)
# Add existing children to queue
asset_items_queue.append((asset_id, child_item))
new_items = []
for asset_id in children_ids:
# Look for item in cache (maybe parent changed)
item = self._items_by_asset_id.get(asset_id)
# Create new item if was not found
if item is None:
item = QtGui.QStandardItem()
item.setEditable(False)
item.setData(asset_id, ASSET_ID_ROLE)
self._items_by_asset_id[asset_id] = item
new_items.append(item)
# Add item to queue
asset_items_queue.append((asset_id, item))
if new_items:
parent_item.appendRows(new_items)
# Remove cache of removed items
for asset_id in removed_asset_ids:
self._items_by_asset_id.pop(asset_id)
# Refresh data
# - all items refresh all data except id
for asset_id, item in self._items_by_asset_id.items():
asset_doc = asset_docs_by_id[asset_id]
asset_name = asset_doc["name"]
if item.data(ASSET_NAME_ROLE) != asset_name:
item.setData(asset_name, ASSET_NAME_ROLE)
asset_data = asset_doc.get("data") or {}
asset_label = asset_data.get("label") or asset_name
if item.data(ASSET_LABEL_ROLE) != asset_label:
item.setData(asset_label, QtCore.Qt.DisplayRole)
item.setData(asset_label, ASSET_LABEL_ROLE)
has_children = item.rowCount() > 0
icon = get_asset_icon(asset_doc, has_children)
item.setData(icon, QtCore.Qt.DecorationRole)
def _threaded_fetch(self):
asset_docs = self._fetch_asset_docs()
if not self._refreshing:
return
self._doc_payload = asset_docs
# Emit doc fetched only if was not stopped
self._doc_fetched.emit()
def _fetch_asset_docs(self):
project_name = self.get_project_name()
if not project_name:
return []
project_entity = ayon_api.get_project(project_name, fields=["name"])
if not project_entity:
return []
# Get all assets sorted by name
return list(
get_assets(project_name, fields=self._asset_projection.keys())
)
def _stop_fetch_thread(self):
self._refreshing = False
if self._doc_fetching_thread is not None:
while self._doc_fetching_thread.isRunning():
time.sleep(0.01)
self._doc_fetching_thread = None
class _AssetsWidget(QtWidgets.QWidget):
"""Base widget to display a tree of assets with filter.
Assets have only one column and are sorted by name.
Refreshing of assets happens in thread so calling 'refresh' method
is not sequential. To capture moment when refreshing is finished listen
to 'refreshed' signal.
To capture selection changes listen to 'selection_changed' signal. It won't
send any information about new selection as it may be different based on
inheritance changes.
Args:
parent (QWidget): Parent Qt widget.
"""
# on model refresh
refresh_triggered = QtCore.Signal()
refreshed = QtCore.Signal()
# on view selection change
selection_changed = QtCore.Signal()
# It was double clicked on view
double_clicked = QtCore.Signal()
def __init__(self, parent=None):
super(_AssetsWidget, self).__init__(parent=parent)
# Tree View
model = self._create_source_model()
proxy = self._create_proxy_model(model)
view = _AssetsView(self)
view.setModel(proxy)
header_widget = QtWidgets.QWidget(self)
current_asset_icon = qtawesome.icon(
"fa.arrow-down", color=get_default_tools_icon_color()
)
current_asset_btn = QtWidgets.QPushButton(header_widget)
current_asset_btn.setIcon(current_asset_icon)
current_asset_btn.setToolTip("Go to Asset from current Session")
# Hide by default
current_asset_btn.setVisible(False)
refresh_icon = qtawesome.icon(
"fa.refresh", color=get_default_tools_icon_color()
)
refresh_btn = QtWidgets.QPushButton(header_widget)
refresh_btn.setIcon(refresh_icon)
refresh_btn.setToolTip("Refresh items")
filter_input = PlaceholderLineEdit(header_widget)
filter_input.setPlaceholderText("Filter folders..")
# Header
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.addWidget(filter_input)
header_layout.addWidget(current_asset_btn)
header_layout.addWidget(refresh_btn)
# Make header widgets expand vertically if there is a place
for widget in (
current_asset_btn,
refresh_btn,
filter_input,
):
size_policy = widget.sizePolicy()
size_policy.setVerticalPolicy(
QtWidgets.QSizePolicy.MinimumExpanding)
widget.setSizePolicy(size_policy)
# Layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(header_widget, 0)
layout.addWidget(view, 1)
# Signals/Slots
filter_input.textChanged.connect(self._on_filter_text_change)
selection_model = view.selectionModel()
selection_model.selectionChanged.connect(self._on_selection_change)
refresh_btn.clicked.connect(self.refresh)
current_asset_btn.clicked.connect(self._on_current_asset_click)
view.doubleClicked.connect(self.double_clicked)
self._header_widget = header_widget
self._filter_input = filter_input
self._refresh_btn = refresh_btn
self._current_asset_btn = current_asset_btn
self._model = model
self._proxy = proxy
self._view = view
self._last_btns_height = None
self._current_folder_path = None
self.model_selection = {}
@property
def header_widget(self):
return self._header_widget
def get_project_name(self):
self._model.get_project_name()
def set_project_name(self, project_name, refresh=True):
self._model.set_project_name(project_name, refresh)
def set_current_asset_name(self, asset_name):
self._current_folder_path = asset_name
def _create_source_model(self):
model = _AssetModel(parent=self)
model.refreshed.connect(self._on_model_refresh)
return model
def _create_proxy_model(self, source_model):
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(source_model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
return proxy
@property
def refreshing(self):
return self._model.refreshing
def refresh(self):
self._refresh_model()
def stop_refresh(self):
self._model.stop_refresh()
def _get_current_folder_path(self):
return self._current_folder_path
def _on_current_asset_click(self):
"""Trigger change of asset to current context asset.
This separation gives ability to override this method and use it
in differnt way.
"""
self.select_current_asset()
def select_current_asset(self):
asset_name = self._get_current_folder_path()
if asset_name:
self.select_asset_by_name(asset_name)
def set_refresh_btn_visibility(self, visible=None):
"""Hide set refresh button.
Some tools may have their global refresh button or do not support
refresh at all.
"""
if visible is None:
visible = not self._refresh_btn.isVisible()
self._refresh_btn.setVisible(visible)
def set_current_asset_btn_visibility(self, visible=None):
"""Hide set current asset button.
Not all tools support using of current context asset.
"""
if visible is None:
visible = not self._current_asset_btn.isVisible()
self._current_asset_btn.setVisible(visible)
def select_asset(self, asset_id):
index = self._model.get_index_by_asset_id(asset_id)
new_index = self._proxy.mapFromSource(index)
self._select_indexes([new_index])
def select_asset_by_name(self, asset_name):
index = self._model.get_index_by_asset_name(asset_name)
new_index = self._proxy.mapFromSource(index)
self._select_indexes([new_index])
def activate_flick_charm(self):
self._view.activate_flick_charm()
def deactivate_flick_charm(self):
self._view.deactivate_flick_charm()
def _on_selection_change(self):
self.selection_changed.emit()
def _on_filter_text_change(self, new_text):
self._proxy.setFilterFixedString(new_text)
def _on_model_refresh(self, has_item):
"""This method should be triggered on model refresh.
Default implementation register this callback in '_create_source_model'
so if you're modifying model keep in mind that this method should be
called when refresh is done.
"""
self._proxy.sort(0)
self._set_loading_state(loading=False, empty=not has_item)
self.refreshed.emit()
def _refresh_model(self):
# Store selection
self._set_loading_state(loading=True, empty=True)
# Trigger signal before refresh is called
self.refresh_triggered.emit()
# Refresh model
self._model.refresh()
def _set_loading_state(self, loading, empty):
self._view.set_loading_state(loading, empty)
def _clear_selection(self):
selection_model = self._view.selectionModel()
selection_model.clearSelection()
def _select_indexes(self, indexes):
valid_indexes = [
index
for index in indexes
if index.isValid()
]
if not valid_indexes:
return
selection_model = self._view.selectionModel()
selection_model.clearSelection()
mode = (
QtCore.QItemSelectionModel.Select
| QtCore.QItemSelectionModel.Rows
)
for index in valid_indexes:
self._view.expand(self._proxy.parent(index))
selection_model.select(index, mode)
self._view.setCurrentIndex(valid_indexes[0])
class SingleSelectAssetsWidget(_AssetsWidget):
"""Single selection asset widget.
Contain single selection specific api methods.
Deprecated:
This widget will be removed soon. Please do not use it in new code.
"""
def get_selected_asset_id(self):
"""Currently selected asset id."""
selection_model = self._view.selectionModel()
indexes = selection_model.selectedRows()
for index in indexes:
return index.data(ASSET_ID_ROLE)
return None
def get_selected_asset_name(self):
"""Currently selected asset name."""
selection_model = self._view.selectionModel()
indexes = selection_model.selectedRows()
for index in indexes:
return index.data(ASSET_NAME_ROLE)
return None