Source code for tk_multi_data_validation.widgets.validation_widget
# Copyright (c) 2022 Autodesk, Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the ShotGrid Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.
import sgtk
from sgtk.platform.qt import QtCore, QtGui
from .list_view_auto_height import ListViewAutoHeight
from .validation_details_widget import ValidationDetailsWidget
from ..api.data.validation_rule_type import ValidationRuleType
from ..api.data.validation_rule import ValidationRule
from ..models.validation_rule_model import ValidationRuleModel
from ..models.validation_rule_type_model import ValidationRuleTypeModel
from ..models.validation_rule_proxy_model import ValidationRuleProxyModel
from .shotgrid_overlay_widget import ShotGridOverlayWidget
from ..utils.exceptions import DataValidationError
from ..utils.framework_qtwidgets import (
FilterMenu,
FilterMenuButton,
GroupedItemView,
ViewItemDelegate,
ViewItemAction,
SearchWidget,
SGQToolButton,
SGQWidget,
SGQPushButton,
SGQSplitter,
SGQMenu,
SGQLabel,
SGQProgressBar,
SGQIcon,
)
from ..utils.decorators import wait_cursor
[docs]class ValidationWidget(SGQWidget):
"""
The main widget for the Data Validation App.
This widget displays the provided data validation rules, and provides the user interface to execute the
rule check and fix functions. When data violations are found, a details widget will display the
individual data items that violate the rules.
"""
#
# Settings keys to save and restore the widget properties
#
SETTINGS_PREFIX = "ValidationWidget"
SETTINGS_VIEW_MODE = "{prefix}_view_mode".format(prefix=SETTINGS_PREFIX)
SETTINGS_SHOW_ONLY_ERRORS = "{prefix}_show_only_errors".format(
prefix=SETTINGS_PREFIX
)
SETTINGS_DETAILS_VISIBILITY = "{prefix}_details_visibility".format(
prefix=SETTINGS_PREFIX
)
SETTINGS_VIEW_DETAILS_SPLITTER_STATE = (
"{prefix}_view_details_splitter_state".format(prefix=SETTINGS_PREFIX)
)
SETTINGS_SELECTED_RULE_TYPE_ID = "{prefix}_selected_rule_type_id".format(
prefix=SETTINGS_PREFIX
)
SETTINGS_FILTER_MENU_STATE = "filter_menu_state"
SETTINGS_AUTO_REFRESH = "auto_refresh_state"
#
# List of view modes
#
(
VIEW_MODE_LIST,
VIEW_MODE_GROUPED,
) = range(2)
# Emit signals to indicate that the details widget is about to run an action, and when it has finished
# (this is useful to # show a busy indicator, if the operation takes some time)
details_about_to_execute_action = QtCore.Signal(dict)
details_execute_action_finished = QtCore.Signal(dict)
# Emit signals to start/stop listening to DCC events
start_event_listening = QtCore.Signal()
stop_event_listening = QtCore.Signal()
# Emit signal to reset the app state
reset_event = QtCore.Signal()
def __init__(self, parent, group_rules_by=None, pre_validate_before_actions=True):
"""
Create the validation widget.
:param parent: The parent widget
:type parent: QWidget
:param group_rules_by: The validation rule field that the view will group rules by
:type group_rules_by: str
:param pre_validate_before_actions: True will run validation before executing actions
so that the action callback acts on the most up to date error data. Default True. Default True.
:type pre_validate_before_actions: bool
"""
super(ValidationWidget, self).__init__(
parent, layout_direction=QtGui.QBoxLayout.TopToBottom
)
self._bundle = sgtk.platform.current_bundle()
self._view_mode = self.VIEW_MODE_GROUPED
self._details_on = True
self._rule_type_filter_on = False
self._group_rules_by = group_rules_by
# Flag indicating if validate is run before actions to ensure actions are applied to
# most up to date error data
self._pre_validate_before_actions = pre_validate_before_actions
# Flag indicating whether or not eiether of the Valdiate or Fix All button have been clicked yet.
self._validation_has_run = False
# Flag indicating that we're in the middle of validating all rules
self._is_validating_all = False
# Flag indicating that we're in the middle of fixing all rules
self._is_fixing_all = False
# The current list of rules in progress (to update the progress bar)
self.__progress_rules = []
# The default warning status text
self.__default_warning_text = "Scene changed since last validated"
self.__warning_details = []
# Flag indicating if the auto-refresh is on. Auto-refresh will listen
# for scene changes to reset the validation state when needed or display
# warning messages.
self.__auto_refresh = False
# Custom callbacks for validate and fix all operations. See properties for more details.
# The default methods to validate and fix all will be initialized. If a ValidationManager
# is being used to manage the validation data, then override these callbacks with the
# ValidationManager specific validate and fix functions.
self._validate_all_callback = self._validate_rules
self._validate_rules_callback = self._validate_rules
self._fix_all_callback = self._fix_rules
self._fix_rules_callback = self._fix_rules
# -----------------------------------------------------
# Set up the UI
self._setup_models()
self._setup_ui()
self._connect_signals()
# -----------------------------------------------------
# Initialize the widget visibility
self.turn_on_rule_type_filter(self._rule_type_filter_on)
self._show_details()
# -----------------------------------------------------
# Initialize the widget data
# Set an empty data message until the it is initialized
self._view_overlay_widget.show_message("No validation data")
#########################################################################################################
# Static methods
@wait_cursor
def __execute_menu_action(self, action, callback, kwargs):
"""Execute the menu action and show the busy cursor."""
self.details_about_to_execute_action.emit(action)
try:
return callback(**kwargs)
finally:
self.details_execute_action_finished.emit(action)
#########################################################################################################
# Properties
@property
def view_mode(self):
"""Get or set the current view mode."""
return self._view_mode
@view_mode.setter
def view_mode(self, mode):
self._set_view_mode(mode)
@property
def group_rules_by(self):
"""Get the field to group the validation rules by in the main view."""
return self._group_rules_by
@property
def validate_button(self):
"""
Get the validate all button.
This may be useful to disconnect the default callback for the fix all button clicked signal, to
execute a custom validate all operation.
"""
return self._validate_button
@property
def fix_button(self):
"""
Get the fix all button.
This may be useful to disconnect the default callback for the fix all button clicked signal, to
execute a custom fix all operation.
"""
return self._fix_button
@property
def publish_button(self):
"""
Get the publish button.
"""
return self._publish_button
@property
def validate_rules_callback(self):
"""
Get or set the custom callback triggered when the validate button is clicked.
This property must be a function that accepts a single parameter that is a
ValidationRule object or a list of ValidationRule objects.
"""
return self._validate_rules_callback
@validate_rules_callback.setter
def validate_rules_callback(self, cb):
self._validate_rules_callback = cb
@property
def validate_all_callback(self):
"""
Get or set the custom callback triggered when the validate button is clicked.
This property must be a function that accepts a single parameter that is a list of ValidationRule
objects.
"""
return self._validate_all_callback
@validate_all_callback.setter
def validate_all_callback(self, cb):
self._validate_all_callback = cb
@property
def fix_all_callback(self):
"""
Get or set the custom callback triggered when the fix button is clicked.
This property must be a function that accepts a single parameter that is a list of ValidationRule
objects.
"""
return self._fix_all_callback
@fix_all_callback.setter
def fix_all_callback(self, cb):
self._fix_all_callback = cb
@property
def fix_rules_callback(self):
"""
Get or set the custom callback triggered when the fix button is clicked.
This property must be a function that accepts a single parameter that is a single
ValiationRule object or a list of ValidationRule objects.
"""
return self._fix_rules_callback
@fix_rules_callback.setter
def fix_rules_callback(self, cb):
self._fix_rules_callback = cb
@property
def pre_validate_before_actions(self):
"""
Get or set the property that decides if validation is ran before executing actions
on the current affected (error) objects.
"""
return self._pre_validate_before_actions
@pre_validate_before_actions.setter
def pre_validate_before_actions(self, pre_validate):
self._pre_validate_before_actions = pre_validate
#########################################################################################################
# Public methods
[docs] def save_state(self, settings_manager):
"""
Save the widget state in the settings.
:param settings_manager: The Toolkit settings object to save the widget settings to.
:type settings_manager: UserSettings
"""
settings_manager.store(self.SETTINGS_AUTO_REFRESH, self.__auto_refresh)
settings_manager.store(self.SETTINGS_VIEW_MODE, self._view_mode)
settings_manager.store(
self.SETTINGS_SHOW_ONLY_ERRORS, self._errors_toggle.isChecked()
)
settings_manager.store(
self.SETTINGS_DETAILS_VISIBILITY, self._details_widget.isVisible()
)
rule_type_selected = self._rule_types_view.selectedIndexes()
if rule_type_selected:
rule_type_selected = rule_type_selected[0]
rule_type_id = rule_type_selected.data(
ValidationRuleTypeModel.RULE_TYPE_ID_ROLE
)
settings_manager.store(self.SETTINGS_SELECTED_RULE_TYPE_ID, rule_type_id)
# Flow Production Tracking settings cannot handle byte arrays, so just save it in the QSettings objects
settings_manager.store(
self.SETTINGS_VIEW_DETAILS_SPLITTER_STATE,
self._view_details_splitter.saveState(),
pickle_setting=False,
)
settings_manager.store(
self.SETTINGS_FILTER_MENU_STATE, self._filter_menu.save_state()
)
[docs] def restore_state(self, settings_manager):
"""
Restore the widget state from the settings.
:param settings_manager: The Toolkit settings object to restore the widget settings to.
:type settings_manager: UserSettings
"""
# Restore the filter menu state
menu_state = settings_manager.retrieve(self.SETTINGS_FILTER_MENU_STATE, None)
if not menu_state:
menu_state = {
"{role}.data_type".format(role=ValidationRuleModel.RULE_ITEM_ROLE): {},
"{role}.required".format(role=ValidationRuleModel.RULE_ITEM_ROLE): {},
}
self._filter_menu.restore_state(menu_state)
self.view_mode = settings_manager.retrieve(
self.SETTINGS_VIEW_MODE, self.VIEW_MODE_GROUPED
)
show_only_errors = settings_manager.retrieve(
self.SETTINGS_SHOW_ONLY_ERRORS, False
)
self._toggle_errors(show_only_errors)
show_details = settings_manager.retrieve(
self.SETTINGS_DETAILS_VISIBILITY, False
)
self._show_details(show_details)
if self._rule_type_filter_on:
rule_type_id = settings_manager.retrieve(
self.SETTINGS_SELECTED_RULE_TYPE_ID,
ValidationRuleType.RULE_TYPE_NONE,
)
rule_type_item = self._rule_types_model.get_item_for_rule_type(rule_type_id)
if rule_type_item:
rule_type_index = rule_type_item.index()
else:
rule_type_index = self._rule_types_model.index(0, 0)
self._rule_types_view.selectionModel().select(
rule_type_index,
QtGui.QItemSelectionModel.ClearAndSelect
| QtGui.QItemSelectionModel.Current,
)
auto_refresh = settings_manager.retrieve(
self.SETTINGS_AUTO_REFRESH, self.__auto_refresh
)
self._auto_refresh_option_action.setChecked(auto_refresh)
self._on_toggle_auto_refresh(auto_refresh)
splitter_state = settings_manager.retrieve(
self.SETTINGS_VIEW_DETAILS_SPLITTER_STATE, None
)
self._view_details_splitter.restoreState(splitter_state)
# Must set the splitter collapsible property after the state is restored, or else this property is overwritten
self._view_details_splitter.setChildrenCollapsible(False)
[docs] def turn_on_details(self, on):
"""
Turn details on to show the right-hand side details panel widget.
:param on: Set to True to turn on, or False to turn off.
:type on: bool
"""
self._details_on = on
if self._details_on:
self._details_button.show()
else:
self._details_button.hide()
self._details_widget.hide()
self._details_overlay_widget.hide()
[docs] def turn_on_rule_type_filter(self, on):
"""
Turn rule type filter on to show the left-hand side filter widget.
:param on: Set to True to turn on, or False to turn off.
:type on: bool
"""
self._rule_type_filter_on = on
if self._rule_type_filter_on:
self._rule_types_widget.show()
else:
self._rule_types_widget.hide()
[docs] def set_validation_rules(self, validation_rules, validation_rule_types=None):
"""
Set the validation rule types and data for the widget.
This will reinitialize the validation rule models with the given data.
:param validation_rules: The validation rules data
:type validation_rules: list<ValidationRule>
:param validation_rule_types: The type of validation rules to group the data by
:type validation_rule_types: list<ValidationRuleType>
"""
# Reset the widget UI before setting the new data
self.reset()
if self._rule_type_filter_on:
rule_types = validation_rule_types or []
if not rule_types:
# Not rule types provied, extract the rule types from the data
for rule in validation_rules:
rule_types.append(rule.type)
if rule_types:
self._rule_types_model.initialize_data(rule_types)
self._rules_model.initialize_data(validation_rules)
[docs] def get_active_rules(self):
"""
Get the list of the validation rules that are currently active.
A validation rule is considered to be active if it is visible in the current view. Validation
operations, like "Validate" and "Fix All", may only be applied to active rules.
:return: The list of validation rules.
:rtype: list<ValidationRule>
"""
rules = []
src_model = self._rules_proxy_model.sourceModel()
for proxy_row in range(self._rules_proxy_model.rowCount()):
proxy_index = self._rules_proxy_model.index(proxy_row, 0)
src_index = self._rules_proxy_model.mapToSource(proxy_index)
rule = src_index.data(ValidationRuleModel.RULE_ITEM_ROLE)
if rule:
rules.append(rule)
# Check if the source index has children
src_model_item = src_model.itemFromIndex(src_index)
child_rows = src_model_item.rowCount()
for child_row in range(child_rows):
child_item = src_model_item.child(child_row)
# Need to check that child is visible (proxy model has accepted it)
if self._rules_proxy_model.filterAcceptsRow(child_row, src_index):
rule = child_item.data(ValidationRuleModel.RULE_ITEM_ROLE)
if rule:
rules.append(rule)
return rules
[docs] def show_validation_error(self, show=True, text=None):
"""
Show the validation warning.
The validation warning indicates that the scene has changed since the last validation.
Showing the validation warning will display a warning icon and text describing the
warning.
:param show: True will show the validation warning, False will hide it.
:type show: bool
:param text: Additional warning details to display.
:type text: str
"""
if not show or text is None:
self._error_msg_label.setText("")
self._error_msg_widget.hide()
else:
current_text = self._error_msg_label.text()
error_text = f"<span style='color:#EB5555;'><b>{text}</b></span>"
if current_text:
error_text = f"{current_text}<br>{error_text}"
self._error_msg_label.setText(error_text)
self._error_msg_widget.show()
[docs] def show_validation_warning(self, show=True, text=None):
"""
Show the validation warning.
The validation warning indicates that the scene has changed since the last validation.
Showing the validation warning will display a warning icon and text describing the
warning.
:param show: True will show the validation warning, False will hide it.
:type show: bool
:param text: Additional warning details to display.
:type text: str
"""
if not show:
self.__warning_widget.hide()
self.__warning_details = []
elif self._validation_has_run:
# Only set warning if the validation has already run
if not self.__warning_widget.isVisible():
self.__warning_widget.show()
if not self.__warning_details:
self.__warning_details.append("")
if text and text not in self.__warning_details:
self.__warning_details.append(text)
warning = self.__default_warning_text + "\n - ".join(
self.__warning_details
)
self.__warning_label.setText(warning)
[docs] def reset(self):
"""Reset the validation state of the widget."""
self._validation_has_run = False
self.show_validation_warning(False)
self.__progress_bar_text.setText("Click Validate or Fix All to start.")
######################################################################################################
# Protected methods
def _setup_models(self):
"""
Set up the models for the widget.
This should be called once when creating the widget, and it should be called before _setup_ui.
"""
# Rule types left-hand list (filter) navigation
self._rule_types_model = ValidationRuleTypeModel(self)
# Main validation rule source and proxy models
self._rules_model = ValidationRuleModel(self, self.group_rules_by, self._bundle)
self._rules_proxy_model = ValidationRuleProxyModel(self)
self._rules_proxy_model.setSourceModel(self._rules_model)
def _setup_ui(self):
"""
Set up the widget UI.
This should be called once when creating the widget.
"""
# -----------------------------------------------------
# Main content
# Create the left-hand list filter navigation
# TODO allow the list filter to be configurable (not only by rule types)
self._rule_types_view = ListViewAutoHeight(self, height_padding=6)
self._rule_types_view.setModel(self._rule_types_model)
self._rule_types_view.setMouseTracking(True)
self._rule_types_view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self._rule_types_view.group_items_selectable = True
self._rule_types_view.setMinimumWidth(200)
self._rule_types_view.setSizePolicy(
QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Fixed)
)
self._rule_types_delegate = self._create_rule_types_delegate()
self._rule_types_widget = SGQWidget(
self,
layout_direction=QtGui.QBoxLayout.TopToBottom,
child_widgets=[self._rule_types_view, None],
)
# Create horizontal splitter for main view and details widgets
self._view_details_splitter = SGQSplitter(self)
self._view_details_splitter.setSizePolicy(
QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
)
self._view_details_splitter.setOrientation(QtCore.Qt.Horizontal)
self._rules_view = GroupedItemView(self._view_details_splitter)
self._rules_view.setMinimumWidth(300)
self._rules_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self._rules_view.setMouseTracking(True)
self._rules_view.group_items_selectable = True
self._rules_view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self._rules_view.setModel(self._rules_proxy_model)
self._rules_delegate = self._create_rules_delegate()
self._view_overlay_widget = ShotGridOverlayWidget(self._rules_view)
self._details_widget = ValidationDetailsWidget(self._view_details_splitter)
self._details_overlay_widget = ShotGridOverlayWidget(self._details_widget)
# Place the splitter in a container widget so that the left hand rule types widget and the splitter
# widget vertically align
rules_widget = SGQWidget(
self,
layout_direction=QtGui.QBoxLayout.TopToBottom,
child_widgets=[self._view_details_splitter],
)
# Create a layout for the main content and add the widgets
self._content_widget = SGQWidget(
self,
layout_direction=QtGui.QBoxLayout.LeftToRight,
child_widgets=[self._rule_types_widget, rules_widget],
)
# -----------------------------------------------------
# Top toolbar
# Set up the refresh button menu
reset_action = QtGui.QAction(SGQIcon.refresh(), "Reset", self)
reset_action.setToolTip("Reset the validation state")
reset_action.triggered.connect(self.reset_event)
self._auto_refresh_option_action = QtGui.QAction("Turn On Auto-Refresh", self)
self._auto_refresh_option_action.setCheckable(True)
self._auto_refresh_option_action.setToolTip(
"Auto-refresh will listen for scene changes to reset the validation state when needed or display warning messages."
)
self._auto_refresh_option_action.triggered.connect(self._on_toggle_auto_refresh)
reset_menu_btn = QtGui.QMenu(self)
reset_menu_btn.addActions(
[
reset_action,
self._auto_refresh_option_action,
]
)
self.reset_btn = SGQToolButton()
self.reset_btn.setObjectName("reset_btn")
self.reset_btn.setIcon(SGQIcon.refresh())
self.reset_btn.setCheckable(True)
self.reset_btn.setMenu(reset_menu_btn)
self.reset_btn.setPopupMode(QtGui.QToolButton.MenuButtonPopup)
self.reset_btn.clicked.connect(self._on_reset_clicked)
# Validation status widget
self.__warning_label = SGQLabel(self.__default_warning_text)
self.__warning_widget = SGQWidget(
self,
child_widgets=[
SGQToolButton(self, SGQIcon.validation_warning()),
self.__warning_label,
],
)
self.__warning_widget.hide()
# List view mode button
self._view_mode_list_button = SGQToolButton(self, icon=SGQIcon.list_view_mode())
self._view_mode_list_button.setObjectName("view_mode_list_button")
self._view_mode_list_button.setToolTip("Compact List View")
# Grouped view mode button
self._view_mode_grouped_button = SGQToolButton(
self, icon=SGQIcon.grid_view_mode()
)
self._view_mode_grouped_button.setObjectName("view_mode_grouped_button")
self._view_mode_grouped_button.setToolTip("Grouped View")
# Error view mode button
self._errors_toggle = SGQToolButton(self, icon=SGQIcon.toggle())
self._errors_toggle.setObjectName("errors_toggle")
self._errors_toggle.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self._errors_toggle.setText(" Only show validation errors")
self._errors_toggle.setToolTip("Toggle to see only validation errors.")
self._errors_toggle.setIconSize(QtCore.QSize(25, 14))
self._errors_toggle.setCursor(QtCore.Qt.PointingHandCursor)
# Number of errors label
self._errors_label = SGQLabel(self)
# Details button
self._details_button = SGQToolButton(self, icon=SGQIcon.info())
self._details_button.setToolTip("Show/Hide Details Panel")
# Filter text search bar
self._search_text_widget = SearchWidget(self)
self._search_text_widget.setMaximumWidth(150)
# Filter menu
self._filter_menu = FilterMenu(self, refresh_on_show=False)
self._filter_menu.set_filter_roles(
[
self._rules_model.RULE_ITEM_ROLE,
]
)
self._filter_menu.set_accept_fields(
[
"{role}.data_type".format(role=ValidationRuleModel.RULE_ITEM_ROLE),
"{role}.required".format(role=ValidationRuleModel.RULE_ITEM_ROLE),
]
)
self._filter_menu.set_filter_model(self._rules_proxy_model)
# Filter menu button
self._filter_menu_button = FilterMenuButton()
self._filter_menu_button.setMenu(self._filter_menu)
# Create the top toolbar layout and add all the widgets
self._toolbar_widget = SGQWidget(
self,
child_widgets=[
self.reset_btn,
self.__warning_widget,
None,
self._view_mode_list_button,
self._view_mode_grouped_button,
self._search_text_widget,
self._filter_menu_button,
self._details_button,
],
)
self._error_msg_label = SGQLabel(self)
self._error_msg_label.setWordWrap(True)
self._error_msg_label.setSizePolicy(
QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum)
)
self._error_msg_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self._error_msg_widget = SGQWidget(
self,
child_widgets=[
self._error_msg_label,
],
)
self._error_msg_label.setSizePolicy(
QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)
)
self._error_msg_widget.hide()
# -----------------------------------------------------
# Bottom toolbar
#
self._validate_button = SGQPushButton("Validate")
self._fix_button = SGQPushButton("Fix All")
self._publish_button = SGQPushButton("Ready to Publish")
# Create the button layout and add the widgets
self._footer_widget = SGQWidget(
self,
child_widgets=[
self._errors_toggle,
None,
self._errors_label,
self._validate_button,
self._fix_button,
self._publish_button,
],
)
# -----------------------------------------------------
# Progress bar
self.__progress_bar = SGQProgressBar()
self.__progress_bar_text = SGQLabel("Click Validate or Fix All to start.")
self.__progress_bar_widget = SGQWidget(
self,
child_widgets=[
self.__progress_bar,
self.__progress_bar_text,
],
layout_direction=QtGui.QBoxLayout.TopToBottom,
)
# -----------------------------------------------------
# Layouts and main widget
#
self._error_msg_widget.layout().setContentsMargins(10, 0, 10, 10)
self._toolbar_widget.layout().setContentsMargins(10, 0, 10, 0)
self._content_widget.layout().setContentsMargins(0, 0, 0, 0)
self._footer_widget.layout().setContentsMargins(10, 0, 10, 0)
self.__progress_bar_widget.layout().setContentsMargins(10, 10, 10, 0)
self.__warning_widget.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setSpacing(0)
self.add_widgets(
[
self._error_msg_widget,
self._toolbar_widget,
self._content_widget,
self._footer_widget,
self.__progress_bar_widget,
]
)
def _connect_signals(self):
"""
Set up and connect signal slots between widgets.
This should be called once when creating the widget.
"""
# -----------------------------------------------------
# Button signals
#
self._view_mode_list_button.clicked.connect(
lambda checked=None: self._set_view_mode(self.VIEW_MODE_LIST)
)
self._view_mode_grouped_button.clicked.connect(
lambda checked=None: self._set_view_mode(self.VIEW_MODE_GROUPED)
)
self._details_button.clicked.connect(
lambda checked=None: self._show_details(checked)
)
self._errors_toggle.clicked.connect(lambda checked=None: self._toggle_errors())
self._search_text_widget.search_edited.connect(self._on_search_text_changed)
# -----------------------------------------------------
# Rule types view signals
#
self._rule_types_view.selectionModel().selectionChanged.connect(
self._on_rule_type_selection_changed
)
# -----------------------------------------------------
# Rules view signals
#
self._rules_view.selectionModel().selectionChanged.connect(
self._on_rule_selection_changed
)
self._rules_view.customContextMenuRequested.connect(
self._on_rule_item_context_menu_requested
)
self._rules_view.doubleClicked.connect(self._on_rule_item_double_clicked)
# -----------------------------------------------------
# Rules types model signals
#
self._rule_types_model.modelReset.connect(self._rule_types_view.updateGeometry)
self._rule_types_model.rule_type_check_state_changed.connect(
self._on_rule_type_check_state_changed
)
# -----------------------------------------------------
# Rules model signals
#
self._rules_model.modelReset.connect(self._on_rules_model_reset)
self._rules_model.rule_check_state_changed.connect(
self._on_rule_check_state_changed
)
# -----------------------------------------------------
# Rules proxy model signals
#
self._rules_proxy_model.layoutChanged.connect(self._on_rules_proxy_model_reset)
# -----------------------------------------------------
# Details widget signals
#
self._details_widget.request_validate_data.connect(
lambda rule: self.on_validate_rules(rule, refresh_details=True)
)
self._details_widget.request_fix_data.connect(self.on_fix_rules)
self._details_widget.about_to_execute_action.connect(
self.details_about_to_execute_action
)
self._details_widget.execute_action_finished.connect(
self.details_execute_action_finished
)
# -----------------------------------------------------
# Button clicked signals
#
self.validate_button.clicked.connect(self.on_validate_all)
self.fix_button.clicked.connect(self.on_fix_all)
def _create_rule_types_delegate(self):
"""
Create the delegate for the rule types view and set it.
"""
assert (
self._rule_types_view
), "The rules view must be created before the delegate"
delegate = ViewItemDelegate(self._rule_types_view)
delegate.text_padding = ViewItemDelegate.Padding(13, 7, 0, 7)
delegate.item_padding = ViewItemDelegate.Padding(4, 4, 0, 4)
delegate.separator_role = ValidationRuleTypeModel.VIEW_ITEM_SEPARATOR_ROLE
delegate.add_action(
{
"type": ViewItemAction.TYPE_ICON,
"show_always": True,
"get_data": get_rule_type_icon_data,
},
ViewItemDelegate.LEFT,
)
delegate.add_actions(
[
{
"type": ViewItemAction.TYPE_CHECK_BOX,
"show_always": True,
"padding_right": 24,
"padding_top": 0,
"padding_bottom": 0,
"get_data": get_rule_type_checkbox_data,
},
{
"type": ViewItemAction.TYPE_ICON,
"show_always": True,
"get_data": get_rule_type_status_icon_data,
},
],
ViewItemDelegate.RIGHT,
)
self._rule_types_view.setItemDelegate(delegate)
return delegate
def _create_rules_delegate(self):
"""
Create the delegate for the rules view and set it.
"""
assert self._rules_view, "The rules view must be created before the delegate"
delegate = ViewItemDelegate(self._rules_view)
delegate.text_rect_valign = ViewItemDelegate.CENTER
delegate.elide_text = False
delegate.elide_header = True
delegate.header_role = ValidationRuleModel.VIEW_ITEM_HEADER_ROLE
delegate.separator_role = ValidationRuleModel.VIEW_ITEM_SEPARATOR_ROLE
delegate.loading_role = ValidationRuleModel.VIEW_ITEM_LOADING_ROLE
delegate.height_role = ValidationRuleModel.VIEW_ITEM_HEIGHT_ROLE
delegate.expand_role = ValidationRuleModel.VIEW_ITEM_EXPAND_ROLE
delegate.text_role = ValidationRuleModel.VIEW_ITEM_TEXT_ROLE
delegate.add_action(
{
"type": ViewItemAction.TYPE_PUSH_BUTTON,
"icon": SGQIcon.tree_arrow(),
"show_always": True,
"features": QtGui.QStyleOptionButton.Flat,
"get_data": get_expand_action_data,
"callback": lambda view, index, pos: view.toggle_expand(index),
},
ViewItemDelegate.LEFT,
)
delegate.add_actions(
[
{
"type": ViewItemAction.TYPE_CHECK_BOX,
"show_always": True,
"padding_top": 0,
"padding_bottom": 0,
"padding_right": 0,
"get_data": get_rule_optional_data,
},
{
"type": ViewItemAction.TYPE_CHECK_BOX,
"check_state_role": ValidationRuleModel.RULE_MANUAL_CHECK_STATE_ROLE,
"show_always": True,
"padding_top": 0,
"padding_bottom": 0,
"padding_right": 14,
"get_data": get_rule_manual_data,
},
{
"type": ViewItemAction.TYPE_PUSH_BUTTON,
"name": "...",
"padding_left": 4,
"padding_right": 4,
"padding": 2,
"get_data": get_rule_show_actions_data,
"callback": self.rule_show_actions_callback,
},
{
"type": ViewItemAction.TYPE_PUSH_BUTTON,
"padding": 2,
"get_data": get_rule_fix_action_data,
"callback": self.rule_fix_action_callback,
},
{
"type": ViewItemAction.TYPE_PUSH_BUTTON,
"padding": 2,
"get_data": get_rule_check_action_data,
"callback": self.rule_check_action_callback,
},
],
ViewItemDelegate.FLOAT_TOP_RIGHT,
)
delegate.add_actions(
[
{
"type": ViewItemAction.TYPE_ICON,
"icon_size": QtCore.QSize(20, 20),
"show_always": True,
"padding": 2,
"get_data": get_rule_status_action_data,
},
],
ViewItemDelegate.LEFT,
)
self._rules_view.setItemDelegate(delegate)
return delegate
def _set_view_mode(self, view_mode):
"""
Set the current view mode for the main validation rules view.
:param view_mode: The view mode (id) to set
Supported view modes:
VIEW_MODE_LIST - a flat list view
VIEW_MODE_GROUPED - a grouped list view
:type view_mode: int
"""
self._view_mode = view_mode
if view_mode == self.VIEW_MODE_LIST:
self._view_mode_list_button.setChecked(True)
self._view_mode_grouped_button.setChecked(False)
self._rules_model.hierarchical = False
self._rules_view.group_spacing = 30
self._details_widget.show_description = True
self._rules_delegate.text_padding = ViewItemDelegate.Padding(8, 10, 8, 10)
self._rules_delegate.action_item_margin = 4
self._rules_delegate.visible_lines = 1
elif view_mode == self.VIEW_MODE_GROUPED:
self._view_mode_grouped_button.setChecked(True)
self._view_mode_list_button.setChecked(False)
self._rules_model.hierarchical = True
self._rules_view.group_spacing = 4
self._details_widget.show_description = False
self._rules_delegate.visible_lines = -1
self._rules_delegate.text_padding = ViewItemDelegate.Padding(10, 10, 10, 10)
self._rules_delegate.action_item_margin = 7
else:
assert False, "Unsupported view mode"
def _show_details(self, show=None):
"""
Set the details widget visibility and update the widget according to the current selection.
If the details functionality is turned off, nothing will happen.
:param show: True will show the details, else False will hide it. If None, the details visibility
will be toggled based on the current visibility state. Default=None
:type show: bool
"""
if not self._details_on:
# The details widget is not available
return
if show is None:
# Toggle the visibility
show = self._details_button.isChecked()
else:
# Set the visibility explicitly
self._details_button.setChecked(show)
self._details_widget.setVisible(show)
if show:
indexes = self._rules_view.selectionModel().selectedIndexes()
self._set_details(indexes)
def _set_details(self, selected_indexes):
"""
Set the details widget with the current seleciton.
:param selected_indexes: The selected indexes to update the widget with
:type selected_indexes: list<QtGui.QModelIndex>
"""
if not selected_indexes:
self._details_overlay_widget.show_message(
"Select a Validation Rule to see more details."
)
elif len(selected_indexes) > 1:
self._details_overlay_widget.show_message(
"Select a Validation Rule to see details."
)
else:
self._details_overlay_widget.hide()
model_index = selected_indexes[0]
rule = model_index.data(ValidationRuleModel.RULE_ITEM_ROLE)
if not rule:
# Do not show details for group items
self._details_overlay_widget.show_message(
"Select a Validation Rule to see more details."
)
else:
self._details_widget.set_data(rule)
def _refresh_details(self, rule=None):
"""
Refresh the details widget to reflect the latest changes to the data.
"""
if not self._details_on or not self._details_widget.isVisible():
return
if (
rule
and self._details_widget.rule
and self._details_widget.rule.id != rule.id
):
return
self._details_widget.refresh()
def _update_errors(self):
"""
Update the errors label text to indicated how many errors ther are currently.
"""
num_errors = len(self._rules_model.get_errors())
if num_errors:
self._errors_label.setText(
"{} issue{} found ".format(num_errors, "s" if num_errors > 1 else "")
)
self._errors_label.show()
else:
self._errors_label.hide()
def _start_progress(self, rules, text=None):
"""
Start showing progress of the current operation.
:param rules: The list of rules that are being operated on.
:type rules: List[ValidationRule]
:param text: Optional text to display with the progress bar.
:type text: str
"""
if not rules:
return
if self.__progress_rules:
# Already in progress, append to the current progress
self.__progress_rules.extend(rules)
start_value = self.__progress_bar.value()
else:
# Set the current list of rules in progress
self.__progress_rules = rules
start_value = 0
# Set up the progress bar widget and text
self.__progress_bar.setRange(0, len(self.__progress_rules))
self.__progress_bar.setValue(start_value)
if text is not None:
self.__progress_bar_text.setText(text)
def _update_progress(self, rule, increment, text=None):
"""
Update the progress of the current operation.
:param rule: The rule that is currently being operated on.
:type rule: ValidationRule
:param increment: True to increment the progress else False to keep the progress the same.
:type increment: bool
:param text: Optional text to display with the progress bar.
:type text: str
"""
if text is not None:
self.__progress_bar_text.setText(text or "")
if increment:
if rule not in self.__progress_rules:
# Extend the progress bar maximum value if the rule being operated on was
# not originally in scope (e.g. a dependency rule that was not in the original list)
self.__progress_bar.setMaximum(self.__progress_bar.maximum() + 1)
# Increment the progress bar value
self.__progress_bar.setValue(self.__progress_bar.value() + 1)
def _reset_progress(self, text=None):
"""
Reset the progress to the default state.
This should be called once an operation has ended.
:param text: Optional text to display with the progress bar.
:type text: str
"""
# Reset the current list of rules in progress
self.__progress_rules = []
# Reset the progress bar value and text
self.__progress_bar.reset()
self.__progress_bar_text.setText(text or "")
def _show_context_menu(self, widget, pos, indexes=None):
"""
Show a context menu for the selected items.
:param widget: The source widget.
:type widget: QtGui.QWidget
:param pos: The position for the context menu relative to the source widget.
:type pos: QtCore.QPoint
"""
if not indexes:
selection_model = self._rules_view.selectionModel()
if selection_model:
indexes = selection_model.selectedIndexes()
# A single index must be selected
if not indexes or len(indexes) > 1:
return
# Get the actions for this index
src_index = _ensure_source_index(indexes[0])
rule_model_item = src_index.model().itemFromIndex(src_index)
rule_actions = rule_model_item.data(ValidationRuleModel.RULE_ACTIONS_ROLE)
actions = []
for rule_action in rule_actions:
callback = rule_action.get("callback")
if not callback:
continue
rule = rule_model_item.data(ValidationRuleModel.RULE_ITEM_ROLE)
kwargs = rule_action.get("kwargs", {})
kwargs["errors"] = (
rule.get_errors() if self.pre_validate_before_actions else rule.errors
)
action = QtGui.QAction(rule_action["name"])
action.triggered.connect(
lambda fn=callback, k=kwargs: self.__execute_menu_action(
rule_action, fn, k
)
)
actions.append(action)
# Add action to show details for the item that the context menu is shown for.
is_details_visible = self._details_widget.isVisible()
toggle_details_action = QtGui.QAction(
"Hide Details" if is_details_visible else "Show Details"
)
toggle_details_action.triggered.connect(
lambda: self._show_details(show=not is_details_visible)
)
actions.append(toggle_details_action)
# Create the menu, add the actions and show it
menu = SGQMenu(self)
menu.addActions(actions)
pos = widget.mapToGlobal(pos)
menu.exec_(pos)
@sgtk.LogManager.log_timing
def _validate_rules(self, rules, refresh_details=False):
"""
The default validate operation that executes the validate function for each of the rules.
:param rules: The list of rules, or single rule, to validate.
:type rules: list<ValidationRule> | ValidationRule
:param refresh_details: Set to True to refresh the details widget after the validation.
:type refresh_details: bool
"""
if not isinstance(rules, list):
rules = [rules]
for rule in rules:
self._bundle.logger.debug("Validating Rule: {}".format(rule.name))
self.validate_rule_begin(rule)
rule.exec_check()
self.validate_rule_finished(rule, update_rule_type=False)
if refresh_details:
# Refresh the details since its data may have changed
self._refresh_details()
@sgtk.LogManager.log_timing
def _fix_rules(self, rules):
"""
The default fix operation that executes the fix function for each of the rules.
NOTE this default fix operation does not take into account rule dependencies. Rules will be executed
in the order as they appear in the model. Use the ValidationManager to implement rule resolution order
based on dependenceis.
:param rules: The list of rules to validate.
:type rules: list<ValidationRule>
"""
if not isinstance(rules, list):
rules = [rules]
for rule in rules:
self._bundle.logger.debug("Resolving Rule: {}".format(rule.name))
self.fix_rule_begin(rule)
rule.exec_fix()
self.fix_rule_finished(rule)
def _update_view_overlay(self):
"""Update the main rules view overlay widget."""
if self._rules_model.rowCount() <= 0:
self._view_overlay_widget.show_message("No data.")
elif self._rules_proxy_model.rowCount() <= 0:
# There are no results showing, display an overlay message
icon = None
title = None
details_text = None
if self._errors_toggle.isChecked():
# Showing only errors
if not self._validation_has_run:
title = "Click Validate or Fix All to start."
else:
num_errors = len(self._rules_model.get_errors())
if num_errors <= 0:
icon = SGQIcon.validation_ok(size=SGQIcon.SIZE_40x40)
details_text = "<br/>".join(
[
"<span style='font-size:24px; color:#309AFF;'>Awesome work!</span>",
"",
"<span style='font-size:14px;'>Success! No errors found. You're ready to publish.</span>",
]
)
else:
# There are errors but they are hidden by the current filters.
title = "No results. Clear filters to view data."
else:
# Showing all data
# The current filters must be hiding everything.
title = "No results. Clear filters to view data."
self._view_overlay_widget.show_message(
title=title, image=icon, details=details_text
)
else:
self._view_overlay_widget.hide()
######################################################################################################
# Callback methods
[docs] @wait_cursor
def on_validate_all(self):
"""
Callback triggered when the validation all button has been triggered.
If the custom validate all callback is set, then get all rules from the model to resolve and pass it
to the custom callback, else call the default validate all operation.
"""
active_rules = self.get_active_rules()
self.validate_all_begin(active_rules)
try:
self.validate_all_callback(active_rules)
finally:
self.validate_all_finished()
[docs] @wait_cursor
def on_fix_all(self):
"""
Callback triggered when the fix all button has been triggered.
If the custom fix all callback is set, then get all rules from the model to resolve and pass it to
the custom callback, else call the default fix all operation.
"""
active_rules = self.get_active_rules()
self.fix_all_begin(active_rules)
try:
self.fix_all_callback(active_rules)
finally:
self.fix_all_finished()
[docs] @wait_cursor
def on_validate_rules(self, rules, refresh_details=False):
"""
Callback triggered to validate a specific set of rules.
:param rule: The validation rule or list of rules to validate.
:type rule: VaildationRule | list<ValidationRule>
:param refresh_details: Set to True to refresh the details widget after validation.
:type refresh_details: bool
"""
if isinstance(rules, ValidationRule):
rules = [rules]
if not isinstance(rules, (list, tuple)):
raise DataValidationError(
"Rules passed to validate must be a list or tuple."
)
if not self.validate_rules_callback:
raise DataValidationError("Validation rules callback not defined.")
self.validate_rules_callback(rules)
if refresh_details:
# Refresh the details since its data may have changed
self._refresh_details()
[docs] @wait_cursor
def on_fix_rules(self, rules):
"""
Callback triggered to fix a specific set of rules.
:param rule: The validation rule or list of rules to fix.
:type rule: VaildationRule | list<ValidationRule>
"""
if isinstance(rules, ValidationRule):
rules = [rules]
if not isinstance(rules, (list, tuple)):
raise DataValidationError("Rules passed to fix must be a list or tuple.")
if not self.fix_rules_callback:
raise DataValidationError("Fix rules callback not defined.")
self.fix_rules_callback(rules)
[docs] def validate_rule_begin(self, rule):
"""
Call this method before a validaiton rule is check function is executed.
:param rule: The rule that is about to start executing its check function.
:type rule: ValidationRule
"""
if not rule:
return
self._update_progress(rule, False, f"Validating: {rule.name}")
[docs] def validate_rule_finished(self, rule, update_rule_type=True):
"""
Call this method after a validation rule check function has finished executing.
:param rule: The rule that finished executing its check function.
:type rule: ValidationRule
:param update_rule_type: True will update the rule type model data based on the updated rule.
:type update_rule_type: bool
"""
self._update_progress(rule, True)
if not rule or self._is_validating_all:
# Do not process the individual rule after validation if there is not rule, or all rules are
# being validated at once
return
rule_item = self._rules_model.get_item_for_rule(rule)
if not rule_item:
return
updated = False
if update_rule_type:
# Update the rule type status corresponding to the updated rule
status = self._rules_model.get_status_for_rule_type(rule.type)
rule_type_index = self._rule_types_model.get_item_for_rule_type(rule.type)
if rule_type_index:
rule_type_index.setData(
status, ValidationRuleTypeModel.RULE_TYPE_STATUS_ROLE
)
updated = True
if not updated:
rule_item.emitDataChanged()
# Update the proxy model to reflect any changes after validation
self._rules_proxy_model._update()
# Update the errors label to reflect any changes after validation
self._update_errors()
self._refresh_details(rule)
[docs] def validate_all_begin(self, rules=None):
"""
Call this method before all validation rules are checked.
:param rules: The list of rules that are about to be validated.
:type rules: List[ValidationRule]
"""
if self._is_validating_all:
# Already validating
return
# Pause event listening during the validate operation
if self.__auto_refresh:
self.stop_event_listening.emit()
self._is_validating_all = True
self.show_validation_error(show=False)
self._start_progress(rules, "Begin validating...")
[docs] def validate_all_finished(self):
"""Call this method after all validation rules have been checked."""
if not self._is_validating_all:
# Nothing to do, if validation did not happen.
return
# Get and update the rule type statuses
(
valid,
errors,
incomplete,
) = self._rules_model.get_statuses_for_rule_type()
self._rule_types_model.set_statuses(valid, errors, incomplete)
# Update the flags around validation before emitting/triggering any signals/slots, in
# case they rely on these flags being updated
self._validation_has_run = True
self._is_validating_all = False
# Emit signal that validation rules have been updated
self._rules_model.emit_all_data_changed()
# Update the proxy model to reflect any changes after validation
self._rules_proxy_model._update()
# Update the errors label to reflect any changes after validation
self._update_errors()
# Ensure the details is refreshed
self._refresh_details()
self._reset_progress("Completed validation")
# Reset any validation warnings now that validation is complete
self.show_validation_warning(False)
# Turn event listening back on
if self.__auto_refresh:
self.start_event_listening.emit()
[docs] def fix_all_begin(self, rules=None):
"""
Call this method before all validation rules are fixed.
:param rules: The list of rules that are about to be fixed.
:type rules: List[ValidationRule]
"""
if self._is_fixing_all:
# Already fixing
return
# Pause event listening during the fix operation
if self.__auto_refresh:
self.stop_event_listening.emit()
self._is_fixing_all = True
self.show_validation_error(show=False)
self._start_progress(rules, "Begin fixing...")
[docs] def fix_all_finished(self):
"""Call this method after all validation rules have been fixed."""
if not self._is_fixing_all:
return
self._is_fixing_all = False
self._reset_progress("Completed all fixes")
self.show_validation_warning(False)
# Turn event listening back on
if self.__auto_refresh:
self.start_event_listening.emit()
[docs] def fix_rule_begin(self, rule):
"""
Call this method before a validaiton rule is fix function is executed.
:param rule: The rule that is about to start executing its fix function.
:type rule: ValidationRule
"""
if not rule:
return
# Update the progress bar with the current rule that is getting fixed
self._update_progress(rule, False, f"Fixing: {rule.name}")
[docs] def fix_rule_finished(self, rule):
"""
Call this method after a validation rule fix function has finished executing.
:param rule: The rule that finished executing its fix function.
:type rule: ValidationRule
"""
if not rule:
return
# Update the progress bar after a rule has finished fixing
self._update_progress(rule, True)
rule_item = self._rules_model.get_item_for_rule(rule)
if not rule_item:
return
# The rule data may have changed, emit the data changed signal to indicate rule data updates.
rule_item.emitDataChanged()
def _on_rule_item_context_menu_requested(self, pos):
"""
Callback triggered when a rule from the view has been right-clicked.
:param pos: The mouse position, relative to the sender (widget), captured when triggering this
callback.
:type pos: QtCore.QPoint
"""
self._show_context_menu(self.sender(), pos)
def _on_rule_item_double_clicked(self, index):
"""
Callback triggered when a rule from the view has been double-clicked.
:param index: The index the mouse double-clicked on.
:type index: QModelIndex
"""
rule = index.data(ValidationRuleModel.RULE_ITEM_ROLE)
self.on_validate_rules(rule, refresh_details=True)
def _on_search_text_changed(self):
"""
Callback triggered when the search widget text has been updated.
"""
search_text = self._search_text_widget._get_search_text()
self._rules_proxy_model.set_text_filter_value(
search_text, data_func=self._rules_delegate.get_displayed_text
)
def _on_rules_model_reset(self):
"""
Callback triggered when the rules model has been reset.
"""
self._update_view_overlay()
self._filter_menu.refresh(force=True)
def _on_rules_proxy_model_reset(self):
"""
Callback triggered when the rules proxy model has been reset.
"""
self._update_view_overlay()
def _on_rule_check_state_changed(self, rule, check_state):
"""
Callback triggered when a rule has been checked.
When a rule is checked, this will affect the data of the rule types model.
:param rule: The valudaiton rule whose check state changed
:type rule: ValidationRule
"""
# Update the rule types model data
self._rule_types_model.set_check_state_for_rule_type(rule.type, check_state)
def _on_rule_type_check_state_changed(self, rule_type, check_state):
"""
Callback triggered when a rule type has been checked.
When a rule type is checked, thsi will affect the data of the rules model.
:param rule_type: The validation rule type whose check state changed
:type rule_type: ValidationRuleType
"""
if rule_type.id == ValidationRuleType.RULE_TYPE_MANUAL:
check_state_role = ValidationRuleModel.RULE_MANUAL_CHECK_STATE_ROLE
else:
check_state_role = QtCore.Qt.CheckStateRole
self._rules_model.set_check_state_for_rule_type(
rule_type, check_state, check_state_role
)
def _on_rule_type_selection_changed(self):
"""
Callback triggered when the rule type selection has changed.
The rules list will be updated to reflect the current rule type selection.
"""
indexes = self._rule_types_view.selectionModel().selectedIndexes()
if not indexes:
return
# Only single selection
index = indexes[0]
# Get the value to filter the rule type by
rule_type = index.data(ValidationRuleTypeModel.RULE_TYPE_ROLE)
# Filter the rules view by the selected rule type
if rule_type is None or rule_type.id == ValidationRuleType.RULE_TYPE_NONE:
self._rules_proxy_model.remove_rule_type_filter()
else:
self._rules_proxy_model.set_rule_type(
rule_type,
filter_role=ValidationRuleModel.RULE_ITEM_ROLE,
)
def _on_rule_selection_changed(self):
"""
Callback triggered when the rule selection has chagned.
The details widget will be updated to reflect the current rule selection.
"""
indexes = self._rules_view.selectionModel().selectedIndexes()
self._set_details(indexes)
def _on_reset_clicked(self, checked=False):
"""Slot triggered when the reset button has been clicked."""
# Keep the button state in sync with the auto-refresh state
self.reset_btn.setChecked(self.__auto_refresh)
self.reset_event.emit()
def _on_toggle_auto_refresh(self, checked):
"""
Slot triggered when the auto-refresh option value changed from the reset menu.
:param checked: True if auto-refresh is checked, else False.
:type checked: bool
"""
self.__auto_refresh = checked
self.reset_btn.setChecked(self.__auto_refresh)
# Start/stop listening for DCC change events
if self.__auto_refresh:
self.start_event_listening.emit()
else:
self.stop_event_listening.emit()
def _toggle_errors(self, show_errors=None):
"""
Callback triggered to show only the validation rules with errors.
:param show_errors: Set to True to show only errors, False to show all, and None to toggle the current state.
:type show_errors: bool
"""
if show_errors:
self._errors_toggle.setChecked(show_errors)
self._rules_proxy_model.turn_on_error_filter(on=show_errors)
self._filter_menu.refresh()
######################################################################################################
# ViewItemDelegate callback functions
#
[docs] def rule_show_actions_callback(self, view, index, pos):
"""
The validation rule action to show the list of action items was triggered.
:param view: The view the index belongs to.
:type view: QAbstractItemView
:param index: The index to act on
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:param pos: The mouse position captured on triggered this callback
:type pos: :class:`sgtk.platform.qt.QtCore.QPoint`
"""
# First select the index
self._rules_view.selectionModel().select(
index, QtGui.QItemSelectionModel.ClearAndSelect
)
self._show_context_menu(view, pos, [index])
[docs] @wait_cursor
def rule_check_action_callback(self, view, index, pos):
"""
The validation rule action to execute the rule check function was triggered.
:param view: The view the index belongs to.
:type view: QAbstractItemView
:param index: The index to act on
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:param pos: The mouse position captured on triggered this callback
:type pos: :class:`sgtk.platform.qt.QtCore.QPoint`
"""
# First select the index
self._rules_view.selectionModel().select(
index, QtGui.QItemSelectionModel.ClearAndSelect
)
# Get the ValidationRule objects for the index
rules = index.data(ValidationRuleModel.RULE_ITEM_ROLE) or index.data(
ValidationRuleModel.RULE_ITEMS_ROLE
)
self.on_validate_rules(rules, refresh_details=True)
[docs] @wait_cursor
def rule_fix_action_callback(self, view, index, pos):
"""
The validation rule action to execute the rule fix function was triggered.
:param view: The view the index belongs to.
:type view: QAbstractItemView
:param index: The index to act on
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:param pos: The mouse position captured on triggered this callback
:type pos: :class:`sgtk.platform.qt.QtCore.QPoint`
"""
# First select the index
self._rules_view.selectionModel().select(
index, QtGui.QItemSelectionModel.ClearAndSelect
)
# Get the ValidationRule object for the index
rules = index.data(ValidationRuleModel.RULE_ITEM_ROLE) or index.data(
ValidationRuleModel.RULE_ITEMS_ROLE
)
self.on_fix_rules(rules)
#############################################################################################################
# ViewItemDelegate callback functions (independent of the ValidationWidget class)
#
def get_expand_action_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying the rule group header expand action.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
visible = index.data(ValidationRuleModel.IS_GROUP_ITEM_ROLE)
state = QtGui.QStyle.State_Active | QtGui.QStyle.State_Enabled
if parent.is_expanded(index):
state |= QtGui.QStyle.State_Off
else:
state |= QtGui.QStyle.State_On
return {"visible": visible, "state": state}
def get_rule_type_icon_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying the rule type icon.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
icon = index.data(ValidationRuleTypeModel.RULE_TYPE_ICON_ROLE)
visible = bool(icon)
return {"visible": visible, "icon": icon}
def get_rule_type_status_icon_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying the rule type status.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
icon = index.data(ValidationRuleTypeModel.RULE_TYPE_STATUS_ICON_ROLE)
visible = bool(icon)
return {"visible": visible, "icon": icon}
def get_rule_type_checkbox_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying the rule type checkbox widget.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
rule_type_id = index.data(ValidationRuleTypeModel.RULE_TYPE_ID_ROLE)
checkbox_state = index.data(QtCore.Qt.CheckStateRole)
icon = index.data(ValidationRuleTypeModel.CHECKBOX_ICON_ROLE)
# This action is "visible" for all to align actions in each row, but for rule types that this
# action does not apply to, it will be hidden (but take up the space)
visible = True
applicable = rule_type_id in (
ValidationRuleType.RULE_TYPE_MANUAL,
ValidationRuleType.RULE_TYPE_OPTIONAL,
)
state = QtGui.QStyle.State_Active | QtGui.QStyle.State_Enabled
if checkbox_state == QtCore.Qt.Checked:
state |= QtGui.QStyle.State_On
elif checkbox_state == QtCore.Qt.PartiallyChecked:
if icon:
# Icons cannot have a partial check state - set the state to be on
state |= QtGui.QStyle.State_On
else:
state |= QtGui.QStyle.State_NoChange
else:
state |= QtGui.QStyle.State_Off
return {
"visible": visible,
"placeholder": not applicable,
"state": state,
"icon": icon,
"padding_right": 0 if icon else 14,
}
def get_rule_manual_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying a manual rule.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
rule = index.data(ValidationRuleModel.RULE_ITEM_ROLE)
if not rule:
return {"visible": False}
if not rule.manual:
return {"visible": False}
checkbox_state = index.data(ValidationRuleModel.RULE_MANUAL_CHECK_STATE_ROLE)
state = QtGui.QStyle.State_Active | QtGui.QStyle.State_Enabled
if checkbox_state == QtCore.Qt.Checked:
state |= QtGui.QStyle.State_On
elif checkbox_state == QtCore.Qt.PartiallyChecked:
state |= QtGui.QStyle.State_NoChange
else:
state |= QtGui.QStyle.State_Off
return {
"visible": True,
"state": state,
}
def get_rule_optional_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying an optional rule.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
rule = index.data(ValidationRuleModel.RULE_ITEM_ROLE)
if not rule:
return {"visible": False}
if not rule.optional:
if rule.manual:
# Ensure the manual checkboxes are aligned across rows
return {"visible": True, "placeholder": True, "padding_left": 22}
return {"visible": False}
checkbox_icon = index.data(ValidationRuleModel.CHECKBOX_ICON_ROLE)
checkbox_state = index.data(QtCore.Qt.CheckStateRole)
state = QtGui.QStyle.State_Active | QtGui.QStyle.State_Enabled
if checkbox_state == QtCore.Qt.Checked:
state |= QtGui.QStyle.State_On
elif checkbox_state == QtCore.Qt.PartiallyChecked:
state |= QtGui.QStyle.State_NoChange
else:
state |= QtGui.QStyle.State_Off
return {
"visible": True,
"state": state,
"icon": checkbox_icon,
}
def get_rule_show_actions_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying and executing the rule action items.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
visible = bool(index.data(ValidationRuleModel.RULE_ACTIONS_ROLE))
return {
"visible": visible,
}
def get_rule_check_action_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying and executing the rule check action.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
name = index.data(ValidationRuleModel.RULE_CHECK_NAME_ROLE)
visible = index.data(ValidationRuleModel.RULE_CHECK_FUNC_ROLE) is not None or (
index.data(ValidationRuleModel.IS_GROUP_ITEM_ROLE)
and index.data(ValidationRuleModel.RULE_ITEMS_ROLE)
)
if name and index.data(ValidationRuleModel.RULE_VALIDATION_RAN):
# Modify the name to prepend "Re", e.g. Validate -> Revalidate
name = "Re{name}".format(name=name.lower())
return {
"visible": visible,
"name": name,
}
def get_rule_fix_action_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying and executing the rule fix action.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
name = index.data(ValidationRuleModel.RULE_FIX_NAME_ROLE)
visible = index.data(ValidationRuleModel.RULE_FIX_FUNC_ROLE) is not None or (
index.data(ValidationRuleModel.IS_GROUP_ITEM_ROLE)
and index.data(ValidationRuleModel.RULE_ITEMS_ROLE)
)
return {
"visible": visible,
"name": name,
}
def get_rule_status_action_data(parent, index):
"""
Callback function triggered by the ViewItemDelegate.
Get the data for displaying the rule status.
:param parent: The parent of the ViewItemDelegate which triggered this callback
:type parent: QAbstractItemView
:param index: The index the action is for.
:type index: :class:`sgtk.platform.qt.QtCore.QModelIndex`
:return: The data for the action and index.
:rtype: dict (see the ViewItemAction class attribute `get_data` for more details)
"""
icon = index.data(ValidationRuleModel.RULE_STATUS_ICON_ROLE)
visible = icon is not None
return {
"visible": visible,
"icon": icon,
}
#############################################################################################################
# Helper functions
def _ensure_source_index(index):
"""
Convenience method to get the source index from the given index.
:param index: The index to ensure is the source index
:type index: QtGui.QModelIndex
:return: The source index for the given index
:rtype: QtGui.QModelIndex
"""
src_model = index.model()
if isinstance(src_model, QtGui.QSortFilterProxyModel):
return src_model.mapToSource(index)
return index