mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 13:24:54 +01:00
906 lines
29 KiB
Python
906 lines
29 KiB
Python
import re
|
|
import uuid
|
|
import copy
|
|
import logging
|
|
import collections
|
|
import time
|
|
|
|
import appdirs
|
|
from qtpy import QtCore, QtGui
|
|
import qtawesome
|
|
|
|
from openpype.client import (
|
|
get_projects,
|
|
get_project,
|
|
get_assets,
|
|
)
|
|
from openpype.lib import JSONSettingRegistry
|
|
from openpype.lib.applications import (
|
|
CUSTOM_LAUNCH_APP_GROUPS,
|
|
ApplicationManager
|
|
)
|
|
from openpype.settings import get_project_settings
|
|
from openpype.pipeline import discover_launcher_actions
|
|
from openpype.tools.utils.lib import (
|
|
DynamicQThread,
|
|
get_project_icon,
|
|
)
|
|
from openpype.tools.utils.assets_widget import (
|
|
AssetModel,
|
|
ASSET_NAME_ROLE
|
|
)
|
|
from openpype.tools.utils.tasks_widget import (
|
|
TasksModel,
|
|
TasksProxyModel,
|
|
TASK_TYPE_ROLE,
|
|
TASK_ASSIGNEE_ROLE
|
|
)
|
|
|
|
from . import lib
|
|
from .constants import (
|
|
ACTION_ROLE,
|
|
GROUP_ROLE,
|
|
VARIANT_GROUP_ROLE,
|
|
ACTION_ID_ROLE,
|
|
FORCE_NOT_OPEN_WORKFILE_ROLE
|
|
)
|
|
from .actions import ApplicationAction
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Must be different than roles in default asset model
|
|
ASSET_TASK_TYPES_ROLE = QtCore.Qt.UserRole + 10
|
|
ASSET_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 11
|
|
|
|
|
|
class ActionModel(QtGui.QStandardItemModel):
|
|
def __init__(self, dbcon, parent=None):
|
|
super(ActionModel, self).__init__(parent=parent)
|
|
self.dbcon = dbcon
|
|
|
|
self.application_manager = ApplicationManager()
|
|
|
|
self.default_icon = qtawesome.icon("fa.cube", color="white")
|
|
# Cache of available actions
|
|
self._registered_actions = list()
|
|
self.items_by_id = {}
|
|
path = appdirs.user_data_dir("openpype", "pypeclub")
|
|
self.launcher_registry = JSONSettingRegistry("launcher", path)
|
|
|
|
try:
|
|
_ = self.launcher_registry.get_item("force_not_open_workfile")
|
|
except ValueError:
|
|
self.launcher_registry.set_item("force_not_open_workfile", [])
|
|
|
|
def discover(self):
|
|
"""Set up Actions cache. Run this for each new project."""
|
|
# Discover all registered actions
|
|
actions = discover_launcher_actions()
|
|
|
|
# Get available project actions and the application actions
|
|
app_actions = self.get_application_actions()
|
|
actions.extend(app_actions)
|
|
|
|
self._registered_actions = actions
|
|
|
|
self.filter_actions()
|
|
|
|
def get_application_actions(self):
|
|
actions = []
|
|
if not self.dbcon.current_project():
|
|
return actions
|
|
|
|
project_name = self.dbcon.active_project()
|
|
project_doc = get_project(project_name, fields=["config.apps"])
|
|
if not project_doc:
|
|
return actions
|
|
|
|
project_settings = get_project_settings(project_name)
|
|
only_available = project_settings["applications"]["only_available"]
|
|
self.application_manager.refresh()
|
|
for app_def in project_doc["config"]["apps"]:
|
|
app_name = app_def["name"]
|
|
app = self.application_manager.applications.get(app_name)
|
|
if not app or not app.enabled:
|
|
continue
|
|
|
|
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
|
|
continue
|
|
|
|
if only_available and not app.find_executable():
|
|
continue
|
|
|
|
# Get from app definition, if not there from app in project
|
|
action = type(
|
|
"app_{}".format(app_name),
|
|
(ApplicationAction,),
|
|
{
|
|
"application": app,
|
|
"name": app.name,
|
|
"label": app.group.label,
|
|
"label_variant": app.label,
|
|
"group": None,
|
|
"icon": app.icon,
|
|
"color": getattr(app, "color", None),
|
|
"order": getattr(app, "order", None) or 0,
|
|
"data": {}
|
|
}
|
|
)
|
|
|
|
actions.append(action)
|
|
return actions
|
|
|
|
def get_icon(self, action, skip_default=False):
|
|
icon = lib.get_action_icon(action)
|
|
if not icon and not skip_default:
|
|
return self.default_icon
|
|
return icon
|
|
|
|
def filter_actions(self):
|
|
self.items_by_id.clear()
|
|
# Validate actions based on compatibility
|
|
self.clear()
|
|
|
|
actions = self.filter_compatible_actions(self._registered_actions)
|
|
|
|
single_actions = []
|
|
varianted_actions = collections.defaultdict(list)
|
|
grouped_actions = collections.defaultdict(list)
|
|
for action in actions:
|
|
# Groups
|
|
group_name = getattr(action, "group", None)
|
|
|
|
# Label variants
|
|
label = getattr(action, "label", None)
|
|
label_variant = getattr(action, "label_variant", None)
|
|
if label_variant and not label:
|
|
print((
|
|
"Invalid action \"{}\" has set `label_variant` to \"{}\""
|
|
", but doesn't have set `label` attribute"
|
|
).format(action.name, label_variant))
|
|
action.label_variant = None
|
|
label_variant = None
|
|
|
|
if group_name:
|
|
grouped_actions[group_name].append(action)
|
|
|
|
elif label_variant:
|
|
varianted_actions[label].append(action)
|
|
else:
|
|
single_actions.append(action)
|
|
|
|
items_by_order = collections.defaultdict(list)
|
|
for label, actions in tuple(varianted_actions.items()):
|
|
if len(actions) == 1:
|
|
varianted_actions.pop(label)
|
|
single_actions.append(actions[0])
|
|
continue
|
|
|
|
icon = None
|
|
order = None
|
|
for action in actions:
|
|
if icon is None:
|
|
_icon = lib.get_action_icon(action)
|
|
if _icon:
|
|
icon = _icon
|
|
|
|
if order is None or action.order < order:
|
|
order = action.order
|
|
|
|
if icon is None:
|
|
icon = self.default_icon
|
|
|
|
item = QtGui.QStandardItem(icon, label)
|
|
item.setData(label, QtCore.Qt.ToolTipRole)
|
|
item.setData(actions, ACTION_ROLE)
|
|
item.setData(True, VARIANT_GROUP_ROLE)
|
|
items_by_order[order].append(item)
|
|
|
|
for action in single_actions:
|
|
icon = self.get_icon(action)
|
|
label = lib.get_action_label(action)
|
|
item = QtGui.QStandardItem(icon, label)
|
|
item.setData(label, QtCore.Qt.ToolTipRole)
|
|
item.setData(action, ACTION_ROLE)
|
|
items_by_order[action.order].append(item)
|
|
|
|
for group_name, actions in grouped_actions.items():
|
|
icon = None
|
|
order = None
|
|
for action in actions:
|
|
if order is None or action.order < order:
|
|
order = action.order
|
|
|
|
if icon is None:
|
|
_icon = lib.get_action_icon(action)
|
|
if _icon:
|
|
icon = _icon
|
|
|
|
if icon is None:
|
|
icon = self.default_icon
|
|
|
|
item = QtGui.QStandardItem(icon, group_name)
|
|
item.setData(actions, ACTION_ROLE)
|
|
item.setData(True, GROUP_ROLE)
|
|
|
|
items_by_order[order].append(item)
|
|
|
|
self.beginResetModel()
|
|
|
|
stored = self.launcher_registry.get_item("force_not_open_workfile")
|
|
items = []
|
|
for order in sorted(items_by_order.keys()):
|
|
for item in items_by_order[order]:
|
|
item_id = str(uuid.uuid4())
|
|
item.setData(item_id, ACTION_ID_ROLE)
|
|
|
|
if self.is_force_not_open_workfile(item,
|
|
stored):
|
|
self.change_action_item(item, True)
|
|
|
|
self.items_by_id[item_id] = item
|
|
items.append(item)
|
|
|
|
self.invisibleRootItem().appendRows(items)
|
|
|
|
self.endResetModel()
|
|
|
|
def filter_compatible_actions(self, actions):
|
|
"""Collect all actions which are compatible with the environment
|
|
|
|
Each compatible action will be translated to a dictionary to ensure
|
|
the action can be visualized in the launcher.
|
|
|
|
Args:
|
|
actions (list): list of classes
|
|
|
|
Returns:
|
|
list: collection of dictionaries sorted on order int he
|
|
"""
|
|
|
|
compatible = []
|
|
_session = copy.deepcopy(self.dbcon.Session)
|
|
session = {
|
|
key: value
|
|
for key, value in _session.items()
|
|
if value
|
|
}
|
|
|
|
for action in actions:
|
|
if action().is_compatible(session):
|
|
compatible.append(action)
|
|
|
|
# Sort by order and name
|
|
return sorted(
|
|
compatible,
|
|
key=lambda action: (action.order, lib.get_action_label(action))
|
|
)
|
|
|
|
def update_force_not_open_workfile_settings(self, is_checked, action_id):
|
|
"""Store/remove config for forcing to skip opening last workfile.
|
|
|
|
Args:
|
|
is_checked (bool): True to add, False to remove
|
|
action_id (str)
|
|
"""
|
|
action_item = self.items_by_id.get(action_id)
|
|
if not action_item:
|
|
return
|
|
|
|
actions = action_item.data(ACTION_ROLE)
|
|
if not isinstance(actions, list):
|
|
actions = [actions]
|
|
|
|
action_actions_data = [
|
|
self._prepare_compare_data(action)
|
|
for action in actions
|
|
]
|
|
|
|
stored = self.launcher_registry.get_item("force_not_open_workfile")
|
|
for actual_data in action_actions_data:
|
|
if is_checked:
|
|
stored.append(actual_data)
|
|
else:
|
|
final_values = []
|
|
for config in stored:
|
|
if config != actual_data:
|
|
final_values.append(config)
|
|
stored = final_values
|
|
|
|
self.launcher_registry.set_item("force_not_open_workfile", stored)
|
|
self.launcher_registry._get_item.cache_clear()
|
|
self.change_action_item(action_item, is_checked)
|
|
|
|
def change_action_item(self, item, checked):
|
|
"""Modifies tooltip and sets if opening of last workfile forbidden"""
|
|
tooltip = item.data(QtCore.Qt.ToolTipRole)
|
|
if checked:
|
|
tooltip += " (Not opening last workfile)"
|
|
|
|
item.setData(tooltip, QtCore.Qt.ToolTipRole)
|
|
item.setData(checked, FORCE_NOT_OPEN_WORKFILE_ROLE)
|
|
|
|
def is_application_action(self, action):
|
|
"""Checks if item is of a ApplicationAction type
|
|
|
|
Args:
|
|
action (action)
|
|
"""
|
|
if isinstance(action, list) and action:
|
|
action = action[0]
|
|
|
|
return ApplicationAction in action.__bases__
|
|
|
|
def is_force_not_open_workfile(self, item, stored):
|
|
"""Checks if application for task is marked to not open workfile
|
|
|
|
There might be specific tasks where is unwanted to open workfile right
|
|
always (broken file, low performance). This allows artist to mark to
|
|
skip opening for combination (project, asset, task_name, app)
|
|
|
|
Args:
|
|
item (QStandardItem)
|
|
stored (list) of dict
|
|
"""
|
|
|
|
actions = item.data(ACTION_ROLE)
|
|
if not isinstance(actions, list):
|
|
actions = [actions]
|
|
|
|
if not self.is_application_action(actions[0]):
|
|
return False
|
|
|
|
action_actions_data = [
|
|
self._prepare_compare_data(action)
|
|
for action in actions
|
|
]
|
|
for config in stored:
|
|
if config in action_actions_data:
|
|
return True
|
|
return False
|
|
|
|
def _prepare_compare_data(self, action):
|
|
compare_data = {}
|
|
if action and action.label:
|
|
compare_data = {
|
|
"app_label": action.label.lower(),
|
|
"project_name": self.dbcon.Session["AVALON_PROJECT"],
|
|
"asset": self.dbcon.Session["AVALON_ASSET"],
|
|
"task_name": self.dbcon.Session["AVALON_TASK"]
|
|
}
|
|
return compare_data
|
|
|
|
|
|
class LauncherModel(QtCore.QObject):
|
|
# Refresh interval of projects
|
|
refresh_interval = 10000
|
|
|
|
# Signals
|
|
# Current project has changed
|
|
project_changed = QtCore.Signal(str)
|
|
# Filters has changed (any)
|
|
filters_changed = QtCore.Signal()
|
|
|
|
# Projects were refreshed
|
|
projects_refreshed = QtCore.Signal()
|
|
|
|
# Signals ONLY for assets model!
|
|
# - other objects should listen to asset model signals
|
|
# Asset refresh started
|
|
assets_refresh_started = QtCore.Signal()
|
|
# Assets refresh finished
|
|
assets_refreshed = QtCore.Signal()
|
|
|
|
# Refresh timer timeout
|
|
# - give ability to tell parent window that this timer still runs
|
|
timer_timeout = QtCore.Signal()
|
|
|
|
# Duplication from AssetsModel with "data.tasks"
|
|
_asset_projection = {
|
|
"name": 1,
|
|
"parent": 1,
|
|
"data.visualParent": 1,
|
|
"data.label": 1,
|
|
"data.icon": 1,
|
|
"data.color": 1,
|
|
"data.tasks": 1
|
|
}
|
|
|
|
def __init__(self, dbcon):
|
|
super(LauncherModel, self).__init__()
|
|
# Refresh timer
|
|
# - should affect only projects
|
|
refresh_timer = QtCore.QTimer()
|
|
refresh_timer.setInterval(self.refresh_interval)
|
|
refresh_timer.timeout.connect(self._on_timeout)
|
|
|
|
self._refresh_timer = refresh_timer
|
|
|
|
# Launcher is active
|
|
self._active = False
|
|
|
|
# Global data
|
|
self._dbcon = dbcon
|
|
# Available project names
|
|
self._project_names = set()
|
|
self._project_docs_by_name = {}
|
|
|
|
# Context data
|
|
self._asset_docs = []
|
|
self._asset_docs_by_id = {}
|
|
self._asset_filter_data_by_id = {}
|
|
self._assignees = set()
|
|
self._task_types = set()
|
|
|
|
# Filters
|
|
self._asset_name_filter = ""
|
|
self._assignee_filters = set()
|
|
self._task_type_filters = set()
|
|
|
|
# Last project for which were assets queried
|
|
self._last_project_name = None
|
|
# Asset refresh thread is running
|
|
self._refreshing_assets = False
|
|
# Asset refresh thread
|
|
self._asset_refresh_thread = None
|
|
|
|
def _on_timeout(self):
|
|
"""Refresh timer timeout."""
|
|
if self._active:
|
|
self.timer_timeout.emit()
|
|
self.refresh_projects()
|
|
|
|
def set_active(self, active):
|
|
"""Window change active state."""
|
|
self._active = active
|
|
|
|
def start_refresh_timer(self, trigger=False):
|
|
"""Start refresh timer."""
|
|
self._refresh_timer.start()
|
|
if trigger:
|
|
self._on_timeout()
|
|
|
|
def stop_refresh_timer(self):
|
|
"""Stop refresh timer."""
|
|
self._refresh_timer.stop()
|
|
|
|
@property
|
|
def project_name(self):
|
|
"""Current project name."""
|
|
return self._dbcon.current_project()
|
|
|
|
@property
|
|
def refreshing_assets(self):
|
|
"""Refreshing thread is running."""
|
|
return self._refreshing_assets
|
|
|
|
@property
|
|
def asset_docs(self):
|
|
"""Access to asset docs."""
|
|
return self._asset_docs
|
|
|
|
@property
|
|
def project_names(self):
|
|
"""Available project names."""
|
|
return self._project_names
|
|
|
|
def get_project_doc(self, project_name):
|
|
return self._project_docs_by_name.get(project_name)
|
|
|
|
@property
|
|
def asset_filter_data_by_id(self):
|
|
"""Prepared filter data by asset id."""
|
|
return self._asset_filter_data_by_id
|
|
|
|
@property
|
|
def assignees(self):
|
|
"""All assignees for all assets in current project."""
|
|
return self._assignees
|
|
|
|
@property
|
|
def task_types(self):
|
|
"""All task types for all assets in current project.
|
|
|
|
TODO: This could be maybe taken from project document where are all
|
|
task types...
|
|
"""
|
|
return self._task_types
|
|
|
|
@property
|
|
def task_type_filters(self):
|
|
"""Currently set task type filters."""
|
|
return self._task_type_filters
|
|
|
|
@property
|
|
def assignee_filters(self):
|
|
"""Currently set assignee filters."""
|
|
return self._assignee_filters
|
|
|
|
@property
|
|
def asset_name_filter(self):
|
|
"""Asset name filter (can be used as regex filter)."""
|
|
return self._asset_name_filter
|
|
|
|
def get_asset_doc(self, asset_id):
|
|
"""Get single asset document by id."""
|
|
return self._asset_docs_by_id.get(asset_id)
|
|
|
|
def set_project_name(self, project_name):
|
|
"""Change project name and refresh asset documents."""
|
|
if project_name == self.project_name:
|
|
return
|
|
self._dbcon.Session["AVALON_PROJECT"] = project_name
|
|
self.project_changed.emit(project_name)
|
|
|
|
self.refresh_assets(force=True)
|
|
|
|
def refresh(self):
|
|
"""Trigger refresh of whole model."""
|
|
self.refresh_projects()
|
|
self.refresh_assets(force=False)
|
|
|
|
def refresh_projects(self):
|
|
"""Refresh projects."""
|
|
current_project = self.project_name
|
|
project_names = set()
|
|
project_docs_by_name = {}
|
|
for project_doc in get_projects():
|
|
project_name = project_doc["name"]
|
|
project_names.add(project_name)
|
|
project_docs_by_name[project_name] = project_doc
|
|
|
|
self._project_docs_by_name = project_docs_by_name
|
|
self._project_names = project_names
|
|
self.projects_refreshed.emit()
|
|
if (
|
|
current_project is not None
|
|
and current_project not in project_names
|
|
):
|
|
self.set_project_name(None)
|
|
|
|
def _set_asset_docs(self, asset_docs=None):
|
|
"""Set asset documents and all related data.
|
|
|
|
Method extract and prepare data needed for assets and tasks widget and
|
|
prepare filtering data.
|
|
"""
|
|
if asset_docs is None:
|
|
asset_docs = []
|
|
|
|
all_task_types = set()
|
|
all_assignees = set()
|
|
asset_docs_by_id = {}
|
|
asset_filter_data_by_id = {}
|
|
for asset_doc in asset_docs:
|
|
task_types = set()
|
|
assignees = set()
|
|
asset_id = asset_doc["_id"]
|
|
asset_docs_by_id[asset_id] = asset_doc
|
|
asset_tasks = asset_doc.get("data", {}).get("tasks")
|
|
asset_filter_data_by_id[asset_id] = {
|
|
"assignees": assignees,
|
|
"task_types": task_types
|
|
}
|
|
if not asset_tasks:
|
|
continue
|
|
|
|
for task_data in asset_tasks.values():
|
|
task_assignees = set()
|
|
_task_assignees = task_data.get("assignees")
|
|
if _task_assignees:
|
|
for assignee in _task_assignees:
|
|
task_assignees.add(assignee["username"])
|
|
|
|
task_type = task_data.get("type")
|
|
if task_assignees:
|
|
assignees |= set(task_assignees)
|
|
if task_type:
|
|
task_types.add(task_type)
|
|
|
|
all_task_types |= task_types
|
|
all_assignees |= assignees
|
|
|
|
self._asset_docs_by_id = asset_docs_by_id
|
|
self._asset_docs = asset_docs
|
|
self._asset_filter_data_by_id = asset_filter_data_by_id
|
|
self._assignees = all_assignees
|
|
self._task_types = all_task_types
|
|
|
|
self.assets_refreshed.emit()
|
|
|
|
def set_task_type_filter(self, task_types):
|
|
"""Change task type filter.
|
|
|
|
Args:
|
|
task_types (set): Set of task types that should be visible.
|
|
Pass empty set to turn filter off.
|
|
"""
|
|
self._task_type_filters = task_types
|
|
self.filters_changed.emit()
|
|
|
|
def set_assignee_filter(self, assignees):
|
|
"""Change assignees filter.
|
|
|
|
Args:
|
|
assignees (set): Set of assignees that should be visible.
|
|
Pass empty set to turn filter off.
|
|
"""
|
|
self._assignee_filters = assignees
|
|
self.filters_changed.emit()
|
|
|
|
def set_asset_name_filter(self, text_filter):
|
|
"""Change asset name filter.
|
|
|
|
Args:
|
|
text_filter (str): Asset name filter. Pass empty string to
|
|
turn filter off.
|
|
"""
|
|
self._asset_name_filter = text_filter
|
|
self.filters_changed.emit()
|
|
|
|
def refresh_assets(self, force=True):
|
|
"""Refresh assets."""
|
|
self.assets_refresh_started.emit()
|
|
|
|
if self.project_name is None:
|
|
self._set_asset_docs()
|
|
return
|
|
|
|
if (
|
|
not force
|
|
and self._last_project_name == self.project_name
|
|
):
|
|
return
|
|
|
|
self._stop_fetch_thread()
|
|
|
|
self._refreshing_assets = True
|
|
self._last_project_name = self.project_name
|
|
self._asset_refresh_thread = DynamicQThread(self._refresh_assets)
|
|
self._asset_refresh_thread.start()
|
|
|
|
def _stop_fetch_thread(self):
|
|
self._refreshing_assets = False
|
|
if self._asset_refresh_thread is not None:
|
|
while self._asset_refresh_thread.isRunning():
|
|
# TODO this is blocking UI should be done in a different way
|
|
time.sleep(0.01)
|
|
self._asset_refresh_thread = None
|
|
|
|
def _refresh_assets(self):
|
|
asset_docs = list(get_assets(
|
|
self._last_project_name, fields=self._asset_projection.keys()
|
|
))
|
|
if not self._refreshing_assets:
|
|
return
|
|
self._refreshing_assets = False
|
|
self._set_asset_docs(asset_docs)
|
|
|
|
|
|
class LauncherTasksProxyModel(TasksProxyModel):
|
|
"""Tasks proxy model with more filtering.
|
|
|
|
TODO:
|
|
This can be (with few modifications) used in default tasks widget too.
|
|
"""
|
|
def __init__(self, launcher_model, *args, **kwargs):
|
|
self._launcher_model = launcher_model
|
|
super(LauncherTasksProxyModel, self).__init__(*args, **kwargs)
|
|
|
|
launcher_model.filters_changed.connect(self._on_filter_change)
|
|
|
|
self._task_types_filter = set()
|
|
self._assignee_filter = set()
|
|
|
|
def _on_filter_change(self):
|
|
self._task_types_filter = self._launcher_model.task_type_filters
|
|
self._assignee_filter = self._launcher_model.assignee_filters
|
|
self.invalidateFilter()
|
|
|
|
def filterAcceptsRow(self, row, parent):
|
|
if not self._task_types_filter and not self._assignee_filter:
|
|
return True
|
|
|
|
model = self.sourceModel()
|
|
source_index = model.index(row, self.filterKeyColumn(), parent)
|
|
if not source_index.isValid():
|
|
return False
|
|
|
|
# Check current index itself
|
|
if self._task_types_filter:
|
|
task_type = model.data(source_index, TASK_TYPE_ROLE)
|
|
if task_type not in self._task_types_filter:
|
|
return False
|
|
|
|
if self._assignee_filter:
|
|
assignee = model.data(source_index, TASK_ASSIGNEE_ROLE)
|
|
if not self._assignee_filter.intersection(assignee):
|
|
return False
|
|
return True
|
|
|
|
|
|
class LauncherTaskModel(TasksModel):
|
|
def __init__(self, launcher_model, *args, **kwargs):
|
|
self._launcher_model = launcher_model
|
|
super(LauncherTaskModel, self).__init__(*args, **kwargs)
|
|
|
|
def _refresh_project_doc(self):
|
|
self._project_doc = self._launcher_model.get_project_doc(
|
|
self._launcher_model.project_name
|
|
)
|
|
|
|
def set_asset_id(self, asset_id):
|
|
asset_doc = None
|
|
if self._context_is_valid():
|
|
asset_doc = self._launcher_model.get_asset_doc(asset_id)
|
|
self._set_asset(asset_doc)
|
|
|
|
|
|
class AssetRecursiveSortFilterModel(QtCore.QSortFilterProxyModel):
|
|
def __init__(self, launcher_model, *args, **kwargs):
|
|
self._launcher_model = launcher_model
|
|
|
|
super(AssetRecursiveSortFilterModel, self).__init__(*args, **kwargs)
|
|
|
|
launcher_model.filters_changed.connect(self._on_filter_change)
|
|
self._name_filter = ""
|
|
self._task_types_filter = set()
|
|
self._assignee_filter = set()
|
|
|
|
def _on_filter_change(self):
|
|
self._name_filter = self._launcher_model.asset_name_filter
|
|
self._task_types_filter = self._launcher_model.task_type_filters
|
|
self._assignee_filter = self._launcher_model.assignee_filters
|
|
self.invalidateFilter()
|
|
|
|
def filterAcceptsRow(self, row, parent):
|
|
if (
|
|
not self._name_filter
|
|
and not self._task_types_filter
|
|
and not self._assignee_filter
|
|
):
|
|
return True
|
|
|
|
model = self.sourceModel()
|
|
source_index = model.index(row, self.filterKeyColumn(), parent)
|
|
if not source_index.isValid():
|
|
return False
|
|
|
|
# Check current index itself
|
|
valid = True
|
|
if self._name_filter:
|
|
name = model.data(source_index, ASSET_NAME_ROLE)
|
|
if (
|
|
name is None
|
|
or not re.search(self._name_filter, name, re.IGNORECASE)
|
|
):
|
|
valid = False
|
|
|
|
if valid and self._task_types_filter:
|
|
task_types = model.data(source_index, ASSET_TASK_TYPES_ROLE)
|
|
if not self._task_types_filter.intersection(task_types):
|
|
valid = False
|
|
|
|
if valid and self._assignee_filter:
|
|
assignee = model.data(source_index, ASSET_ASSIGNEE_ROLE)
|
|
if not self._assignee_filter.intersection(assignee):
|
|
valid = False
|
|
|
|
if valid:
|
|
return True
|
|
|
|
# Check children
|
|
rows = model.rowCount(source_index)
|
|
for child_row in range(rows):
|
|
if self.filterAcceptsRow(child_row, source_index):
|
|
return True
|
|
return False
|
|
|
|
|
|
class LauncherAssetsModel(AssetModel):
|
|
def __init__(self, launcher_model, dbcon, parent=None):
|
|
self._launcher_model = launcher_model
|
|
# Make sure that variable is available (even if is in AssetModel)
|
|
self._last_project_name = None
|
|
|
|
super(LauncherAssetsModel, self).__init__(dbcon, parent)
|
|
|
|
launcher_model.project_changed.connect(self._on_project_change)
|
|
launcher_model.assets_refresh_started.connect(
|
|
self._on_launcher_refresh_start
|
|
)
|
|
launcher_model.assets_refreshed.connect(self._on_launcher_refresh)
|
|
|
|
def _on_launcher_refresh_start(self):
|
|
self._refreshing = True
|
|
project_name = self._launcher_model.project_name
|
|
if self._last_project_name != project_name:
|
|
self._clear_items()
|
|
self._last_project_name = project_name
|
|
|
|
def _on_launcher_refresh(self):
|
|
self._fill_assets(self._launcher_model.asset_docs)
|
|
self._refreshing = False
|
|
self.refreshed.emit(bool(self._items_by_asset_id))
|
|
|
|
def _fill_assets(self, *args, **kwargs):
|
|
super(LauncherAssetsModel, self)._fill_assets(*args, **kwargs)
|
|
asset_filter_data_by_id = self._launcher_model.asset_filter_data_by_id
|
|
for asset_id, item in self._items_by_asset_id.items():
|
|
filter_data = asset_filter_data_by_id.get(asset_id)
|
|
|
|
assignees = filter_data["assignees"]
|
|
task_types = filter_data["task_types"]
|
|
|
|
item.setData(assignees, ASSET_ASSIGNEE_ROLE)
|
|
item.setData(task_types, ASSET_TASK_TYPES_ROLE)
|
|
|
|
def _on_project_change(self):
|
|
self._clear_items()
|
|
|
|
def refresh(self, *args, **kwargs):
|
|
raise ValueError("This is a bug!")
|
|
|
|
def stop_refresh(self, *args, **kwargs):
|
|
raise ValueError("This is a bug!")
|
|
|
|
|
|
class ProjectModel(QtGui.QStandardItemModel):
|
|
"""List of projects"""
|
|
|
|
def __init__(self, launcher_model, parent=None):
|
|
super(ProjectModel, self).__init__(parent=parent)
|
|
|
|
self._launcher_model = launcher_model
|
|
self._project_names = set()
|
|
|
|
launcher_model.projects_refreshed.connect(self._on_refresh)
|
|
|
|
def _on_refresh(self):
|
|
project_names = set(self._launcher_model.project_names)
|
|
origin_project_names = set(self._project_names)
|
|
self._project_names = project_names
|
|
|
|
project_names_to_remove = origin_project_names - project_names
|
|
if project_names_to_remove:
|
|
row_counts = {}
|
|
continuous = None
|
|
for row in range(self.rowCount()):
|
|
index = self.index(row, 0)
|
|
index_name = index.data(QtCore.Qt.DisplayRole)
|
|
if index_name in project_names_to_remove:
|
|
if continuous is None:
|
|
continuous = row
|
|
row_counts[continuous] = 0
|
|
row_counts[continuous] += 1
|
|
else:
|
|
continuous = None
|
|
|
|
for row in reversed(sorted(row_counts.keys())):
|
|
count = row_counts[row]
|
|
self.removeRows(row, count)
|
|
|
|
continuous = None
|
|
row_counts = {}
|
|
for idx, project_name in enumerate(sorted(project_names)):
|
|
if project_name in origin_project_names:
|
|
continuous = None
|
|
continue
|
|
|
|
if continuous is None:
|
|
continuous = idx
|
|
row_counts[continuous] = []
|
|
|
|
row_counts[continuous].append(project_name)
|
|
|
|
for row in reversed(sorted(row_counts.keys())):
|
|
items = []
|
|
for project_name in row_counts[row]:
|
|
project_doc = self._launcher_model.get_project_doc(
|
|
project_name
|
|
)
|
|
icon = get_project_icon(project_doc)
|
|
item = QtGui.QStandardItem(icon, project_name)
|
|
items.append(item)
|
|
|
|
self.invisibleRootItem().insertRows(row, items)
|