From 7e91b0e9a6e194181e11a777d338ea2b4c4d315f Mon Sep 17 00:00:00 2001 From: Michael Dolan Date: Mon, 6 May 2024 06:03:36 -0400 Subject: [PATCH] ocioview updates & fixes (#1966) * Improve ocioview exit, view, and icon UX Signed-off-by: Michael Dolan * Fix message router property routing Signed-off-by: Michael Dolan * Add processor context Signed-off-by: Michael Dolan * Revise item type data descriptions Signed-off-by: Michael Dolan * Split item name from item label Signed-off-by: Michael Dolan * Update processor context direction handling Signed-off-by: Michael Dolan --------- Signed-off-by: Michael Dolan --- src/apps/ocioview/ocioview/constants.py | 5 +- .../ocioview/inspect/code_inspector.py | 47 ++++--- .../ocioview/inspect/curve_inspector.py | 14 +- .../ocioview/inspect/log_inspector.py | 3 +- .../items/active_display_view_edit.py | 3 +- .../ocioview/items/color_space_edit.py | 14 +- .../ocioview/items/color_space_model.py | 13 +- .../ocioview/items/config_item_edit.py | 16 +-- .../ocioview/items/config_item_model.py | 52 ++++---- .../ocioview/items/config_properties_edit.py | 7 +- .../ocioview/items/display_view_edit.py | 3 +- .../ocioview/ocioview/items/file_rule_edit.py | 3 +- .../ocioview/items/file_rule_model.py | 3 +- .../ocioview/ocioview/items/look_model.py | 6 +- .../ocioview/items/named_transform_edit.py | 6 +- .../ocioview/items/named_transform_model.py | 9 +- src/apps/ocioview/ocioview/items/rule_edit.py | 3 +- src/apps/ocioview/ocioview/items/utils.py | 26 +++- src/apps/ocioview/ocioview/items/view_edit.py | 10 +- .../ocioview/ocioview/items/view_model.py | 31 +++-- .../ocioview/items/view_transform_edit.py | 11 +- .../ocioview/items/view_transform_model.py | 13 +- .../ocioview/items/viewing_rule_edit.py | 5 +- .../ocioview/items/viewing_rule_model.py | 3 +- src/apps/ocioview/ocioview/main_window.py | 16 ++- src/apps/ocioview/ocioview/message_router.py | 125 ++++++++++++++---- .../ocioview/ocioview/processor_context.py | 28 ++++ .../ocioview/ocioview/ref_space_manager.py | 5 + .../ocioview/ocioview/transform_manager.py | 58 +++++--- .../ocioview/transforms/transform_edit.py | 2 +- src/apps/ocioview/ocioview/utils.py | 4 +- .../ocioview/ocioview/viewer/image_plane.py | 93 +++++++------ .../ocioview/ocioview/viewer/image_viewer.py | 77 ++++++++--- .../ocioview/ocioview/widgets/structure.py | 7 +- 34 files changed, 489 insertions(+), 232 deletions(-) create mode 100644 src/apps/ocioview/ocioview/processor_context.py diff --git a/src/apps/ocioview/ocioview/constants.py b/src/apps/ocioview/ocioview/constants.py index 4862bdbb80..7a973c7c2e 100644 --- a/src/apps/ocioview/ocioview/constants.py +++ b/src/apps/ocioview/ocioview/constants.py @@ -10,8 +10,9 @@ ROOT_DIR = Path(__file__).parent.parent # Sizes -ICON_SIZE_ITEM = QtCore.QSize(20, 20) -ICON_SIZE_BUTTON = QtCore.QSize(20, 20) +ICON_SIZE_BUTTON = QtCore.QSize(18, 18) +ICON_SIZE_ITEM = QtCore.QSize(18, 18) +ICON_SIZE_TAB = QtCore.QSize(16, 16) ICON_SCALE_FACTOR = 1.15 MARGIN_WIDTH = 13 # Pixels diff --git a/src/apps/ocioview/ocioview/inspect/code_inspector.py b/src/apps/ocioview/ocioview/inspect/code_inspector.py index 22ee1e396e..0c8860672f 100644 --- a/src/apps/ocioview/ocioview/inspect/code_inspector.py +++ b/src/apps/ocioview/ocioview/inspect/code_inspector.py @@ -8,6 +8,7 @@ from pygments.formatters import HtmlFormatter from PySide6 import QtCore, QtGui, QtWidgets +from ..constants import ICON_SIZE_TAB from ..message_router import MessageRouter from ..utils import get_glyph_icon, processor_to_shader_html from ..widgets import EnumComboBox, LogView @@ -26,7 +27,7 @@ def label(cls) -> str: @classmethod def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("mdi6.code-json") + return get_glyph_icon("mdi6.code-json", size=ICON_SIZE_TAB) def __init__(self, parent: Optional[QtCore.QObject] = None): super().__init__(parent=parent) @@ -63,9 +64,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self.gpu_language_box = EnumComboBox(ocio.GpuLanguage) self.gpu_language_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) - self.gpu_language_box.set_member( - MessageRouter.get_instance().get_gpu_language() - ) + self.gpu_language_box.set_member(MessageRouter.get_instance().gpu_language) self.gpu_language_box.currentIndexChanged[int].connect( self._on_gpu_language_changed ) @@ -76,12 +75,20 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): # Layout self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab(self.config_view, get_glyph_icon("mdi6.code-json"), "Config") self.tabs.addTab( - self.ctf_view, get_glyph_icon("mdi6.code-tags"), "Processor (CTF)" + self.config_view, + get_glyph_icon("mdi6.code-json", size=ICON_SIZE_TAB), + "Config", + ) + self.tabs.addTab( + self.ctf_view, + get_glyph_icon("mdi6.code-tags", size=ICON_SIZE_TAB), + "Processor (CTF)", ) self.tabs.addTab( - self.shader_view, get_glyph_icon("mdi6.dots-grid"), "Processor (Shader)" + self.shader_view, + get_glyph_icon("mdi6.dots-grid", size=ICON_SIZE_TAB), + "Processor (Shader)", ) layout = QtWidgets.QVBoxLayout() @@ -188,7 +195,7 @@ def _on_gpu_language_changed(self, index: int) -> None: MessageRouter, which will provide future GPU processors. """ gpu_language = self.gpu_language_box.currentData() - MessageRouter.get_instance().set_gpu_language(gpu_language) + MessageRouter.get_instance().gpu_language = gpu_language if self._prev_gpu_proc is not None: shader_html_data = processor_to_shader_html( self._prev_gpu_proc, gpu_language @@ -212,22 +219,22 @@ def _on_tab_changed(self, index: int) -> None: msg_router = MessageRouter.get_instance() if index == -1: - msg_router.set_config_updates_allowed(False) - msg_router.set_ctf_updates_allowed(False) - msg_router.set_shader_updates_allowed(False) + msg_router.config_updates_allowed = False + msg_router.ctf_updates_allowed = False + msg_router.shader_updates_allowed = False return widget = self.tabs.widget(index) if widget == self.config_view: - msg_router.set_config_updates_allowed(True) - msg_router.set_ctf_updates_allowed(False) - msg_router.set_shader_updates_allowed(False) + msg_router.config_updates_allowed = True + msg_router.ctf_updates_allowed = False + msg_router.shader_updates_allowed = False elif widget == self.ctf_view: - msg_router.set_config_updates_allowed(False) - msg_router.set_ctf_updates_allowed(True) - msg_router.set_shader_updates_allowed(False) + msg_router.config_updates_allowed = False + msg_router.ctf_updates_allowed = True + msg_router.shader_updates_allowed = False elif widget == self.shader_view: - msg_router.set_config_updates_allowed(False) - msg_router.set_ctf_updates_allowed(False) - msg_router.set_shader_updates_allowed(True) + msg_router.config_updates_allowed = False + msg_router.ctf_updates_allowed = False + msg_router.shader_updates_allowed = True diff --git a/src/apps/ocioview/ocioview/inspect/curve_inspector.py b/src/apps/ocioview/ocioview/inspect/curve_inspector.py index 05373742eb..b9013cd3a4 100644 --- a/src/apps/ocioview/ocioview/inspect/curve_inspector.py +++ b/src/apps/ocioview/ocioview/inspect/curve_inspector.py @@ -10,8 +10,9 @@ import PyOpenColorIO as ocio from PySide6 import QtCore, QtGui, QtWidgets -from ..constants import R_COLOR, G_COLOR, B_COLOR, GRAY_COLOR +from ..constants import R_COLOR, G_COLOR, B_COLOR, GRAY_COLOR, ICON_SIZE_TAB from ..message_router import MessageRouter +from ..processor_context import ProcessorContext from ..utils import get_glyph_icon, SignalsBlocked from ..widgets import EnumComboBox, FloatEditArray, IntEdit @@ -39,7 +40,7 @@ def label(cls) -> str: @classmethod def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("mdi6.chart-bell-curve-cumulative") + return get_glyph_icon("mdi6.chart-bell-curve-cumulative", size=ICON_SIZE_TAB) def __init__(self, parent: Optional[QtCore.QObject] = None): super().__init__(parent=parent) @@ -256,14 +257,14 @@ def showEvent(self, event: QtGui.QShowEvent) -> None: super().showEvent(event) msg_router = MessageRouter.get_instance() - msg_router.set_processor_updates_allowed(True) + msg_router.processor_updates_allowed = True def hideEvent(self, event: QtGui.QHideEvent) -> None: """Stop listening for processor updates, if not visible.""" super().hideEvent(event) msg_router = MessageRouter.get_instance() - msg_router.set_processor_updates_allowed(False) + msg_router.processor_updates_allowed = False def resizeEvent(self, event: QtGui.QResizeEvent) -> None: """Re-fit graph on resize, to always be centered.""" @@ -603,10 +604,13 @@ def _fit(self) -> None: self.update() @QtCore.Slot(ocio.CPUProcessor) - def _on_processor_ready(self, cpu_proc: ocio.CPUProcessor) -> None: + def _on_processor_ready( + self, proc_context: ProcessorContext, cpu_proc: ocio.CPUProcessor + ) -> None: """ Update curves from sampled OCIO CPU processor. + :param proc_context: OCIO processor context data :param cpu_proc: CPU processor of currently viewed transform """ self.reset() diff --git a/src/apps/ocioview/ocioview/inspect/log_inspector.py b/src/apps/ocioview/ocioview/inspect/log_inspector.py index b2cf9fa638..b1d5a1c1ae 100644 --- a/src/apps/ocioview/ocioview/inspect/log_inspector.py +++ b/src/apps/ocioview/ocioview/inspect/log_inspector.py @@ -6,6 +6,7 @@ import PyOpenColorIO as ocio from PySide6 import QtCore, QtGui, QtWidgets +from ..constants import ICON_SIZE_TAB from ..log_handlers import set_logging_level from ..message_router import MessageRouter from ..utils import get_glyph_icon @@ -23,7 +24,7 @@ def label(cls) -> str: @classmethod def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("ph.terminal-window") + return get_glyph_icon("ph.terminal-window", size=ICON_SIZE_TAB) def __init__(self, parent: Optional[QtCore.QObject] = None): super().__init__(parent=parent) diff --git a/src/apps/ocioview/ocioview/items/active_display_view_edit.py b/src/apps/ocioview/ocioview/items/active_display_view_edit.py index 593a40d441..76d0ffbefb 100644 --- a/src/apps/ocioview/ocioview/items/active_display_view_edit.py +++ b/src/apps/ocioview/ocioview/items/active_display_view_edit.py @@ -5,6 +5,7 @@ from PySide6 import QtCore, QtGui, QtWidgets +from ..constants import ICON_SIZE_ITEM from ..widgets import ItemModelListWidget from ..utils import get_glyph_icon from .active_display_view_model import ActiveDisplayModel, ActiveViewModel @@ -60,7 +61,7 @@ class ActiveDisplayViewEdit(QtWidgets.QWidget): @classmethod def item_type_icon(cls) -> QtGui.QIcon: - return get_glyph_icon("mdi6.sort-bool-ascending-variant") + return get_glyph_icon("mdi6.sort-bool-ascending-variant", size=ICON_SIZE_ITEM) @classmethod def item_type_label(cls, plural: bool = False) -> str: diff --git a/src/apps/ocioview/ocioview/items/color_space_edit.py b/src/apps/ocioview/ocioview/items/color_space_edit.py index 4e5d659a3f..b93b2b9093 100644 --- a/src/apps/ocioview/ocioview/items/color_space_edit.py +++ b/src/apps/ocioview/ocioview/items/color_space_edit.py @@ -8,6 +8,7 @@ import PyOpenColorIO as ocio from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from ..widgets import ( CheckBox, @@ -39,12 +40,17 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self.reference_space_type_combo = EnumComboBox( ocio.ReferenceSpaceType, icons={ - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun"), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon("ph.monitor"), + ocio.REFERENCE_SPACE_SCENE: get_glyph_icon( + "ph.sun", size=ICON_SIZE_ITEM + ), + ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( + "ph.monitor", size=ICON_SIZE_ITEM + ), }, ) self.aliases_list = StringListWidget( - item_basename="alias", item_icon=get_glyph_icon("ph.bookmark-simple") + item_basename="alias", + item_icon=get_glyph_icon("ph.bookmark-simple", size=ICON_SIZE_ITEM), ) self.family_edit = CallbackComboBox(ConfigCache.get_families, editable=True) self.encoding_edit = CallbackComboBox(ConfigCache.get_encodings, editable=True) @@ -61,7 +67,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): ) self.categories_list = StringListWidget( item_basename="category", - item_icon=get_glyph_icon("ph.bookmarks-simple"), + item_icon=get_glyph_icon("ph.bookmarks-simple", size=ICON_SIZE_ITEM), get_presets=self._get_available_categories, ) diff --git a/src/apps/ocioview/ocioview/items/color_space_model.py b/src/apps/ocioview/ocioview/items/color_space_model.py index 3b6c12d2ce..4e71469888 100644 --- a/src/apps/ocioview/ocioview/items/color_space_model.py +++ b/src/apps/ocioview/ocioview/items/color_space_model.py @@ -8,6 +8,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..ref_space_manager import ReferenceSpaceManager from ..utils import get_enum_member, get_glyph_icon from .config_item_model import ColumnDesc, BaseConfigItemModel @@ -50,8 +51,10 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self._items = ocio.ColorSpaceSet() self._ref_space_icons = { - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun"), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon("ph.monitor"), + ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun", size=ICON_SIZE_ITEM), + ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( + "ph.monitor", size=ICON_SIZE_ITEM + ), } # Update on external config changes, in this case when a required reference @@ -62,10 +65,10 @@ def get_item_names(self) -> list[str]: return [item.getName() for item in self._get_items()] def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get view name from subscription item name - item_name = self.extract_subscription_item_name(item_name) + # Get color space name from subscription item label + item_name = self.extract_subscription_item_name(item_label) ref_space_name = ReferenceSpaceManager.scene_reference_space().getName() return ( diff --git a/src/apps/ocioview/ocioview/items/config_item_edit.py b/src/apps/ocioview/ocioview/items/config_item_edit.py index e28069a6e1..e96a78de06 100644 --- a/src/apps/ocioview/ocioview/items/config_item_edit.py +++ b/src/apps/ocioview/ocioview/items/config_item_edit.py @@ -6,7 +6,7 @@ from PySide6 import QtCore, QtGui, QtWidgets -from ..constants import MARGIN_WIDTH +from ..constants import MARGIN_WIDTH, ICON_SIZE_TAB from ..transform_manager import TransformManager from ..transforms import TransformEditStack from ..utils import get_glyph_icon, SignalsBlocked @@ -50,13 +50,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): no_tf_color = palette.color( palette.ColorGroup.Disabled, palette.ColorRole.Text ) - self._from_ref_icon = get_glyph_icon("mdi6.layers-plus") + self._from_ref_icon = get_glyph_icon("mdi6.layers-plus", size=ICON_SIZE_TAB) self._no_from_ref_icon = get_glyph_icon( - "mdi6.layers-plus", color=no_tf_color + "mdi6.layers-plus", color=no_tf_color, size=ICON_SIZE_TAB ) - self._to_ref_icon = get_glyph_icon("mdi6.layers-minus") + self._to_ref_icon = get_glyph_icon("mdi6.layers-minus", size=ICON_SIZE_TAB) self._no_to_ref_icon = get_glyph_icon( - "mdi6.layers-minus", color=no_tf_color + "mdi6.layers-minus", color=no_tf_color, size=ICON_SIZE_TAB ) # Widgets @@ -316,10 +316,10 @@ def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool: ) ): current_index = self.list.current_index() - item_name = self.model.format_subscription_item_name(current_index) - if item_name: + item_label = self.model.format_subscription_item_label(current_index) + if item_label: TransformManager.set_subscription( - int(event.text()), self.model, item_name + int(event.text()), self.model, item_label ) return True diff --git a/src/apps/ocioview/ocioview/items/config_item_model.py b/src/apps/ocioview/ocioview/items/config_item_model.py index 15a11c9e1e..e77b4a7fcd 100644 --- a/src/apps/ocioview/ocioview/items/config_item_model.py +++ b/src/apps/ocioview/ocioview/items/config_item_model.py @@ -6,9 +6,10 @@ from typing import Any, Optional, Type, Union import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..transform_manager import TransformManager, TransformAgent from ..undo import ItemModelUndoCommand, ConfigSnapshotUndoCommand from ..utils import get_glyph_icon, next_name, item_type_label @@ -74,7 +75,7 @@ def item_type_icon(cls) -> QtGui.QIcon: :return: Item type icon """ if cls.__icon__ is None: - cls.__icon__ = get_glyph_icon(cls.__icon_glyph__) + cls.__icon__ = get_glyph_icon(cls.__icon_glyph__, size=ICON_SIZE_ITEM) return cls.__icon__ @classmethod @@ -510,41 +511,42 @@ def get_item_name(self, index: QtCore.QModelIndex) -> Optional[str]: """ return self.data(self.index(index.row(), self.NAME.column)) - def format_subscription_item_name( + def format_subscription_item_label( self, item_name_or_index: Union[str, QtCore.QModelIndex], **kwargs ) -> Optional[str]: """ - Format item name into a per-model unique name for tracking + Format item name into a per-model unique label for tracking transform subscriptions. :param item_name_or_index: Item name or model index for item - :return: Subscription unique item name + :return: Subscription unique item label """ if isinstance(item_name_or_index, QtCore.QModelIndex): - item_name = self.get_item_name(item_name_or_index) + item_label = self.get_item_name(item_name_or_index) else: - item_name = item_name_or_index - return f"{item_name} [{self.item_type_label().lower()}]" + item_label = item_name_or_index + return f"{item_label} [{self.item_type_label().lower()}]" - def extract_subscription_item_name(self, subscription_item_name: str) -> str: + def extract_subscription_item_name(self, item_label: str) -> str: """ Unformat item name from its per-model unique name for tracking transform subscriptions. - :param subscription_item_name: Subscription unique item name + :param item_label: Subscription unique item label :return: Extracted item name """ suffix = f" [{self.item_type_label().lower()}]" - if subscription_item_name.endswith(suffix): - return subscription_item_name[: -len(suffix)] + if item_label.endswith(suffix): + return item_label[: -len(suffix)] else: - return subscription_item_name + return item_label def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: """ - :param item_name: Item name to get transform for + :param item_label: Subscription unique label of item to get + transform for. :return: Forward and inverse item transforms, or None if either is not defined. """ @@ -661,7 +663,7 @@ def _get_subscription_color( """ slot = TransformManager.get_subscription_slot( self, - self.format_subscription_item_name(self._get_value(item, column_desc)), + self.format_subscription_item_label(self._get_value(item, column_desc)), ) return TransformManager.get_subscription_slot_color( slot, saturation=0.25, value=0.25 @@ -676,7 +678,7 @@ def _get_subscription_icon( """ slot = TransformManager.get_subscription_slot( self, - self.format_subscription_item_name(self._get_value(item, column_desc)), + self.format_subscription_item_label(self._get_value(item, column_desc)), ) return TransformManager.get_subscription_slot_icon(slot) @@ -750,20 +752,24 @@ def _update_tf_subscribers( """ # Name adjustment may be needed for unique item/transform identifiers within # the model. - item_name = self.format_subscription_item_name(item_name) + item_label = self.format_subscription_item_label(item_name) if prev_item_name: - prev_item_name = self.format_subscription_item_name(prev_item_name) + prev_item_label = self.format_subscription_item_label(prev_item_name) + else: + prev_item_label = None # Is item set as a subscription? - slot = TransformManager.get_subscription_slot(self, prev_item_name or item_name) + slot = TransformManager.get_subscription_slot( + self, prev_item_label or item_label + ) if slot != -1: # Broadcast name change - if prev_item_name and prev_item_name != item_name: - self._tf_agents[slot].item_name_changed.emit(item_name) + if prev_item_label and prev_item_label != item_label: + self._tf_agents[slot].item_name_changed.emit(item_label) # Broadcast transform change self._tf_agents[slot].item_tf_changed.emit( - *self.get_item_transforms(item_name) + *self.get_item_transforms(item_label) ) def _get_value(self, item: __item_type__, column_desc: ColumnDesc) -> Any: diff --git a/src/apps/ocioview/ocioview/items/config_properties_edit.py b/src/apps/ocioview/ocioview/items/config_properties_edit.py index 58dddba8d7..ea2bca0354 100644 --- a/src/apps/ocioview/ocioview/items/config_properties_edit.py +++ b/src/apps/ocioview/ocioview/items/config_properties_edit.py @@ -7,7 +7,7 @@ import PyOpenColorIO as ocio from PySide6 import QtWidgets -from ..constants import RGB +from ..constants import RGB, ICON_SIZE_ITEM from ..widgets import ( ComboBox, LineEdit, @@ -40,12 +40,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self.description_edit = TextEdit() self.env_vars_table = StringMapTableWidget( ("Name", "Default Value"), - item_icon=get_glyph_icon("mdi6.variable"), + item_icon=get_glyph_icon("mdi6.variable", size=ICON_SIZE_ITEM), default_key_prefix="ENV_VAR_", default_value="value", ) self.search_path_list = StringListWidget( - item_icon=get_glyph_icon("ph.file-search"), get_item=self._get_search_path + item_icon=get_glyph_icon("ph.file-search", size=ICON_SIZE_ITEM), + get_item=self._get_search_path, ) self.working_dir_edit = PathEdit(QtWidgets.QFileDialog.Directory) self.family_separator_edit = LineEdit() diff --git a/src/apps/ocioview/ocioview/items/display_view_edit.py b/src/apps/ocioview/ocioview/items/display_view_edit.py index 059da46553..40be4a0c01 100644 --- a/src/apps/ocioview/ocioview/items/display_view_edit.py +++ b/src/apps/ocioview/ocioview/items/display_view_edit.py @@ -5,6 +5,7 @@ from PySide6 import QtCore, QtGui, QtWidgets +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from .active_display_view_edit import ActiveDisplayViewEdit from .shared_view_edit import SharedViewEdit @@ -22,7 +23,7 @@ def item_type_icon(cls) -> QtGui.QIcon: """ :return: Item type icon """ - return get_glyph_icon("mdi6.monitor-eye") + return get_glyph_icon("mdi6.monitor-eye", size=ICON_SIZE_ITEM) @classmethod def item_type_label(cls, plural: bool = False) -> str: diff --git a/src/apps/ocioview/ocioview/items/file_rule_edit.py b/src/apps/ocioview/ocioview/items/file_rule_edit.py index 0dd1b8a2f5..95bda2c4b1 100644 --- a/src/apps/ocioview/ocioview/items/file_rule_edit.py +++ b/src/apps/ocioview/ocioview/items/file_rule_edit.py @@ -6,6 +6,7 @@ from PySide6 import QtCore, QtWidgets from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from ..widgets import ( CallbackComboBox, @@ -69,7 +70,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): if file_rule_type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX): custom_keys_table = StringMapTableWidget( ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key"), + item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), default_key_prefix="key_", default_value="value", ) diff --git a/src/apps/ocioview/ocioview/items/file_rule_model.py b/src/apps/ocioview/ocioview/items/file_rule_model.py index d73c76f5a8..2cad0c54ec 100644 --- a/src/apps/ocioview/ocioview/items/file_rule_model.py +++ b/src/apps/ocioview/ocioview/items/file_rule_model.py @@ -9,6 +9,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..undo import ConfigSnapshotUndoCommand from ..utils import get_glyph_icon, next_name from .config_item_model import ColumnDesc, BaseConfigItemModel @@ -77,7 +78,7 @@ def get_rule_type_icon(cls, rule_type: FileRuleType) -> QtGui.QIcon: FileRuleType.RULE_OCIO_V1: "mdi6.contain", FileRuleType.RULE_DEFAULT: "ph.arrow-line-down", } - return get_glyph_icon(glyph_names[rule_type]) + return get_glyph_icon(glyph_names[rule_type], size=ICON_SIZE_ITEM) @classmethod def has_presets(cls) -> bool: diff --git a/src/apps/ocioview/ocioview/items/look_model.py b/src/apps/ocioview/ocioview/items/look_model.py index d7d1a309ea..780a950db2 100644 --- a/src/apps/ocioview/ocioview/items/look_model.py +++ b/src/apps/ocioview/ocioview/items/look_model.py @@ -35,10 +35,10 @@ def get_item_names(self) -> list[str]: return [item.getName() for item in self._get_items()] def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get view name from subscription item name - item_name = self.extract_subscription_item_name(item_name) + # Get look name from subscription item label + item_name = self.extract_subscription_item_name(item_label) scene_ref_name = ReferenceSpaceManager.scene_reference_space().getName() return ( diff --git a/src/apps/ocioview/ocioview/items/named_transform_edit.py b/src/apps/ocioview/ocioview/items/named_transform_edit.py index d2a7bc77a5..fefe69f544 100644 --- a/src/apps/ocioview/ocioview/items/named_transform_edit.py +++ b/src/apps/ocioview/ocioview/items/named_transform_edit.py @@ -6,6 +6,7 @@ from PySide6 import QtWidgets from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from ..widgets import CallbackComboBox, StringListWidget, TextEdit from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit @@ -28,14 +29,15 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): # Widgets self.aliases_list = StringListWidget( - item_basename="alias", item_icon=get_glyph_icon("ph.bookmark-simple") + item_basename="alias", + item_icon=get_glyph_icon("ph.bookmark-simple", size=ICON_SIZE_ITEM), ) self.family_edit = CallbackComboBox(ConfigCache.get_families, editable=True) self.encoding_edit = CallbackComboBox(ConfigCache.get_encodings, editable=True) self.description_edit = TextEdit() self.categories_list = StringListWidget( item_basename="category", - item_icon=get_glyph_icon("ph.bookmarks-simple"), + item_icon=get_glyph_icon("ph.bookmarks-simple", size=ICON_SIZE_ITEM), get_presets=self._get_available_categories, ) diff --git a/src/apps/ocioview/ocioview/items/named_transform_model.py b/src/apps/ocioview/ocioview/items/named_transform_model.py index 2dc02acad3..f42378438e 100644 --- a/src/apps/ocioview/ocioview/items/named_transform_model.py +++ b/src/apps/ocioview/ocioview/items/named_transform_model.py @@ -8,6 +8,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from .config_item_model import ColumnDesc, BaseConfigItemModel @@ -39,16 +40,16 @@ class NamedTransformModel(BaseConfigItemModel): def __init__(self, parent: Optional[QtCore.QObject] = None): super().__init__(parent=parent) - self._item_icon = get_glyph_icon("ph.arrow-square-right") + self._item_icon = get_glyph_icon("ph.arrow-square-right", size=ICON_SIZE_ITEM) def get_item_names(self) -> list[str]: return [item.getName() for item in self._get_items()] def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get view name from subscription item name - item_name = self.extract_subscription_item_name(item_name) + # Get named transform name from subscription item label + item_name = self.extract_subscription_item_name(item_label) config = ocio.GetCurrentConfig() named_transform = config.getNamedTransform(item_name) diff --git a/src/apps/ocioview/ocioview/items/rule_edit.py b/src/apps/ocioview/ocioview/items/rule_edit.py index cb60da1e86..bd759711c2 100644 --- a/src/apps/ocioview/ocioview/items/rule_edit.py +++ b/src/apps/ocioview/ocioview/items/rule_edit.py @@ -5,6 +5,7 @@ from PySide6 import QtCore, QtGui, QtWidgets +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from .file_rule_edit import FileRuleEdit from .utils import adapt_splitter_sizes @@ -22,7 +23,7 @@ def item_type_icon(cls) -> QtGui.QIcon: """ :return: Item type icon """ - return get_glyph_icon("mdi6.list-status") + return get_glyph_icon("mdi6.list-status", size=ICON_SIZE_ITEM) @classmethod def item_type_label(cls, plural: bool = False) -> str: diff --git a/src/apps/ocioview/ocioview/items/utils.py b/src/apps/ocioview/ocioview/items/utils.py index 7e5b6141a1..24ab61256d 100644 --- a/src/apps/ocioview/ocioview/items/utils.py +++ b/src/apps/ocioview/ocioview/items/utils.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +from __future__ import annotations + import enum from typing import Optional @@ -15,17 +17,20 @@ class ViewType(str, enum.Enum): VIEW_SCENE = "View (Scene Reference Space)" -def get_view_type(display: str, view: str) -> ViewType: +def get_view_type(display: str, view: str) -> tuple[ViewType, str | None]: """ Get the view type from a display and view. :param display: Display name. An empty string indicates a shared display. :param view: View name - :return: View type + :return: Tuple of view type and any warning raised while inspecting + the display and view. """ + warning = None + if not display: - return ViewType.VIEW_SHARED + return ViewType.VIEW_SHARED, warning config = ocio.GetCurrentConfig() @@ -35,13 +40,20 @@ def get_view_type(display: str, view: str) -> ViewType: color_space = config.getColorSpace(color_space_name) if color_space is not None: if color_space.getReferenceSpaceType() == ocio.REFERENCE_SPACE_DISPLAY: - return ViewType.VIEW_DISPLAY + if view_transform_name: + warning = ( + f"Invalid view '{display}/{view}' references a view transform " + f"('{view_transform_name}') with a non-display color space " + f"('{color_space_name}'). The view transform will be dropped to " + f"preserve the color space selection." + ) + return ViewType.VIEW_DISPLAY, warning else: - return ViewType.VIEW_SCENE + return ViewType.VIEW_SCENE, warning elif view_transform_name: - return ViewType.VIEW_DISPLAY + return ViewType.VIEW_DISPLAY, warning else: - return ViewType.VIEW_SCENE + return ViewType.VIEW_SCENE, warning def adapt_splitter_sizes(from_sizes: list[int], to_sizes: list[int]) -> list[int]: diff --git a/src/apps/ocioview/ocioview/items/view_edit.py b/src/apps/ocioview/ocioview/items/view_edit.py index 1172dbe754..fecd655f6a 100644 --- a/src/apps/ocioview/ocioview/items/view_edit.py +++ b/src/apps/ocioview/ocioview/items/view_edit.py @@ -260,7 +260,7 @@ def _on_display_editing_finished(self) -> None: signal is emitted twice when pressing enter. See: https://forum.qt.io/topic/39141/qlineedit-editingfinished-signal-is-emitted-twice """ - view_type = self._get_view_type(self.list.current_row()) + view_type, _ = self._get_view_type(self.list.current_row()) self.param_edit.display_edits[view_type].blockSignals(True) self._display_mapper.submit() self.param_edit.display_edits[view_type].blockSignals(False) @@ -276,13 +276,13 @@ def _on_display_renamed(self, display: str, prev_display: str) -> None: """ for i in range(self.model.rowCount()): view_index = self.model.index(i, self.model.NAME.column) - item_name = self.model.format_subscription_item_name(view_index) - prev_item_name = self.model.format_subscription_item_name( + item_label = self.model.format_subscription_item_label(view_index) + prev_item_label = self.model.format_subscription_item_label( view_index, display=prev_display ) - slot = TransformManager.get_subscription_slot(self.model, prev_item_name) + slot = TransformManager.get_subscription_slot(self.model, prev_item_label) if slot != -1: - TransformManager.set_subscription(slot, self.model, item_name) + TransformManager.set_subscription(slot, self.model, item_label) @QtCore.Slot(int) def _on_display_changed(self, display_row: int) -> None: diff --git a/src/apps/ocioview/ocioview/items/view_model.py b/src/apps/ocioview/ocioview/items/view_model.py index 7fd3412630..e4602552f0 100644 --- a/src/apps/ocioview/ocioview/items/view_model.py +++ b/src/apps/ocioview/ocioview/items/view_model.py @@ -7,6 +7,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..ref_space_manager import ReferenceSpaceManager from ..undo import ConfigSnapshotUndoCommand from ..utils import get_glyph_icon, next_name @@ -44,7 +45,7 @@ def get_view_type_icon(cls, view_type: ViewType) -> QtGui.QIcon: ViewType.VIEW_DISPLAY: "mdi6.eye-outline", ViewType.VIEW_SCENE: "mdi6.eye", } - return get_glyph_icon(glyph_names[view_type]) + return get_glyph_icon(glyph_names[view_type], size=ICON_SIZE_ITEM) @classmethod def has_presets(cls) -> bool: @@ -238,12 +239,12 @@ def get_item(self, index: QtCore.QModelIndex) -> Optional[tuple[str, str]]: return None def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: if self._display is not None: - # Get view name from subscription item name - item_name = self.extract_subscription_item_name(item_name) + # Get view name from subscription item label + item_name = self.extract_subscription_item_name(item_label) scene_ref_name = ReferenceSpaceManager.scene_reference_space().getName() return ( @@ -263,20 +264,20 @@ def get_item_transforms( else: return None, None - def format_subscription_item_name( + def format_subscription_item_label( self, item_name_or_index: Union[str, QtCore.QModelIndex], display: Optional[str] = None, **kwargs, ) -> Optional[str]: - item_name = super().format_subscription_item_name(item_name_or_index) - if item_name and (display or self._display): - return f"{display or self._display}/{item_name}" + item_label = super().format_subscription_item_label(item_name_or_index) + if item_label and (display or self._display): + return f"{display or self._display}/{item_label}" else: - return item_name + return item_label - def extract_subscription_item_name(self, subscription_item_name: str) -> str: - item_name = super().extract_subscription_item_name(subscription_item_name) + def extract_subscription_item_name(self, item_label: str) -> str: + item_name = super().extract_subscription_item_name(item_label) if self._display and item_name.startswith(self._display + "/"): item_name = item_name[len(self._display) + 1 :] return item_name @@ -289,7 +290,7 @@ def _get_undo_command_text( # Insert display name before view item_name = self.get_item_name(index) text = text.replace( - f"({item_name})", f"({self.format_subscription_item_name(item_name)})" + f"({item_name})", f"({self.format_subscription_item_label(item_name)})" ) return text @@ -353,9 +354,13 @@ def _get_items(self, preserve: bool = False) -> list[View]: # Display views for name in config.getViews(ocio.VIEW_DISPLAY_DEFINED, self._display): + view_type, warning = get_view_type(self._display, name) + if warning: + self.warning_raised.emit(warning) + self._items.append( View( - get_view_type(self._display, name), + view_type, name, config.getDisplayViewColorSpaceName(self._display, name), config.getDisplayViewTransformName(self._display, name), diff --git a/src/apps/ocioview/ocioview/items/view_transform_edit.py b/src/apps/ocioview/ocioview/items/view_transform_edit.py index c8a0873597..8d0ac15cd4 100644 --- a/src/apps/ocioview/ocioview/items/view_transform_edit.py +++ b/src/apps/ocioview/ocioview/items/view_transform_edit.py @@ -8,6 +8,7 @@ import PyOpenColorIO as ocio from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from ..widgets import EnumComboBox, CallbackComboBox, StringListWidget, TextEdit from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit @@ -32,15 +33,19 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self.reference_space_type_combo = EnumComboBox( ocio.ReferenceSpaceType, icons={ - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun"), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon("ph.monitor"), + ocio.REFERENCE_SPACE_SCENE: get_glyph_icon( + "ph.sun", size=ICON_SIZE_ITEM + ), + ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( + "ph.monitor", size=ICON_SIZE_ITEM + ), }, ) self.family_edit = CallbackComboBox(ConfigCache.get_families, editable=True) self.description_edit = TextEdit() self.categories_list = StringListWidget( item_basename="category", - item_icon=get_glyph_icon("ph.bookmarks-simple"), + item_icon=get_glyph_icon("ph.bookmarks-simple", size=ICON_SIZE_ITEM), get_presets=self._get_available_categories, ) diff --git a/src/apps/ocioview/ocioview/items/view_transform_model.py b/src/apps/ocioview/ocioview/items/view_transform_model.py index 69595ecf69..10f8d5ea22 100644 --- a/src/apps/ocioview/ocioview/items/view_transform_model.py +++ b/src/apps/ocioview/ocioview/items/view_transform_model.py @@ -8,6 +8,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_enum_member, get_glyph_icon from .config_item_model import ColumnDesc, BaseConfigItemModel from .utils import get_scene_to_display_transform, get_display_to_scene_transform @@ -40,18 +41,20 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): super().__init__(parent=parent) self._ref_space_icons = { - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun"), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon("ph.monitor"), + ocio.REFERENCE_SPACE_SCENE: get_glyph_icon("ph.sun", size=ICON_SIZE_ITEM), + ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( + "ph.monitor", size=ICON_SIZE_ITEM + ), } def get_item_names(self) -> list[str]: return [item.getName() for item in self._get_items()] def get_item_transforms( - self, item_name: str + self, item_label: str ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get view name from subscription item name - item_name = self.extract_subscription_item_name(item_name) + # Get view transform name from subscription item label + item_name = self.extract_subscription_item_name(item_label) config = ocio.GetCurrentConfig() view_transform = config.getViewTransform(item_name) diff --git a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py b/src/apps/ocioview/ocioview/items/viewing_rule_edit.py index 83c90a70b2..31aed1bf79 100644 --- a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py +++ b/src/apps/ocioview/ocioview/items/viewing_rule_edit.py @@ -6,6 +6,7 @@ from PySide6 import QtCore, QtWidgets from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..utils import get_glyph_icon from ..widgets import ( FormLayout, @@ -43,7 +44,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self.color_space_list.view.setItemDelegate(ColorSpaceDelegate()) self.custom_keys_table_a = StringMapTableWidget( ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key"), + item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), default_key_prefix="key_", default_value="value", ) @@ -61,7 +62,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): ) self.custom_keys_table_b = StringMapTableWidget( ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key"), + item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), default_key_prefix="key_", default_value="value", ) diff --git a/src/apps/ocioview/ocioview/items/viewing_rule_model.py b/src/apps/ocioview/ocioview/items/viewing_rule_model.py index 7e04efa3d8..4f0c573b10 100644 --- a/src/apps/ocioview/ocioview/items/viewing_rule_model.py +++ b/src/apps/ocioview/ocioview/items/viewing_rule_model.py @@ -10,6 +10,7 @@ from PySide6 import QtCore, QtGui from ..config_cache import ConfigCache +from ..constants import ICON_SIZE_ITEM from ..undo import ConfigSnapshotUndoCommand from ..utils import get_glyph_icon, next_name from .config_item_model import ColumnDesc, BaseConfigItemModel @@ -58,7 +59,7 @@ def get_rule_type_icon(cls, rule_type: ViewingRuleType) -> QtGui.QIcon: ViewingRuleType.RULE_COLOR_SPACE: "ph.swap", ViewingRuleType.RULE_ENCODING: "mdi6.sine-wave", } - return get_glyph_icon(glyph_names[rule_type]) + return get_glyph_icon(glyph_names[rule_type], size=ICON_SIZE_ITEM) @classmethod def has_presets(cls) -> bool: diff --git a/src/apps/ocioview/ocioview/main_window.py b/src/apps/ocioview/ocioview/main_window.py index 69646b2660..c14a0100d9 100644 --- a/src/apps/ocioview/ocioview/main_window.py +++ b/src/apps/ocioview/ocioview/main_window.py @@ -14,6 +14,7 @@ from .constants import ICON_PATH_OCIO from .inspect_dock import InspectDock from .message_router import MessageRouter +from .ref_space_manager import ReferenceSpaceManager from .settings import settings from .undo import undo_stack from .viewer_dock import ViewerDock @@ -147,6 +148,9 @@ def __init__( # Initialize if config_path is not None: self.load_config(config_path) + else: + # New config + self._init_config_tracking() self._update_recent_configs_menu() self._update_window_title() @@ -156,8 +160,10 @@ def __init__( def reset(self) -> None: """ - Reset application, reloading the current OCIO config. + Reset application from the current OCIO config. """ + self._init_config_tracking() + self.config_dock.reset() self.viewer_dock.reset() self.inspect_dock.reset() @@ -189,7 +195,6 @@ def new_config(self) -> None: config = ocio.Config.CreateRaw() ocio.SetCurrentConfig(config) - self.reset() def load_config(self, config_path: Optional[Path] = None) -> None: @@ -227,8 +232,6 @@ def load_config(self, config_path: Optional[Path] = None) -> None: ocio.SetCurrentConfig(config) self.reset() - self._update_cache_id() - def save_config(self) -> bool: """ Save the current OCIO config to the previously loaded config @@ -532,3 +535,8 @@ def _can_close_config(self) -> bool: # No unsaved changes return True + + def _init_config_tracking(self) -> None: + """Setup app-dependent config objects and change tracking.""" + ReferenceSpaceManager.init_reference_spaces() + self._update_cache_id() diff --git a/src/apps/ocioview/ocioview/message_router.py b/src/apps/ocioview/ocioview/message_router.py index 64ee20bec3..efb2c9677a 100644 --- a/src/apps/ocioview/ocioview/message_router.py +++ b/src/apps/ocioview/ocioview/message_router.py @@ -13,6 +13,7 @@ import PyOpenColorIO as ocio from PySide6 import QtCore, QtGui, QtWidgets +from .processor_context import ProcessorContext from .utils import config_to_html, processor_to_ctf_html, processor_to_shader_html @@ -34,7 +35,7 @@ class MessageRunner(QtCore.QObject): config_html_ready = QtCore.Signal(str) ctf_html_ready = QtCore.Signal(str, ocio.GroupTransform) image_ready = QtCore.Signal(np.ndarray) - processor_ready = QtCore.Signal(ocio.CPUProcessor) + processor_ready = QtCore.Signal(ProcessorContext, ocio.CPUProcessor) shader_html_ready = QtCore.Signal(str, ocio.GPUProcessor) LOOP_INTERVAL = 0.5 # In seconds @@ -66,7 +67,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self._gpu_language = ocio.GPU_LANGUAGE_GLSL_4_0 self._prev_config = None - self._prev_cpu_proc = None + self._prev_proc_data = None self._prev_image_array = None self._config_updates_allowed = False @@ -75,59 +76,71 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self._processor_updates_allowed = False self._shader_updates_allowed = False - def get_gpu_language(self) -> ocio.GpuLanguage: + @property + def gpu_language(self) -> ocio.GpuLanguage: return self._gpu_language - def set_gpu_language(self, gpu_language: ocio.GpuLanguage) -> None: + @gpu_language.setter + def gpu_language(self, gpu_language: ocio.GpuLanguage) -> None: self._gpu_language = gpu_language - if self._shader_updates_allowed and self._prev_cpu_proc is not None: + if self._shader_updates_allowed and self._prev_proc_data is not None: # Rebroadcast last processor record - message_queue.put_nowait(self._prev_cpu_proc) + message_queue.put_nowait(self._prev_proc_data) + @property def config_updates_allowed(self) -> bool: return self._config_updates_allowed - def set_config_updates_allowed(self, allowed: bool) -> None: + @config_updates_allowed.setter + def config_updates_allowed(self, allowed: bool) -> None: self._config_updates_allowed = allowed if allowed and self._prev_config is not None: # Rebroadcast last config record message_queue.put_nowait(self._prev_config) + @property def ctf_updates_allowed(self) -> bool: return self._ctf_updates_allowed - def set_ctf_updates_allowed(self, allowed: bool) -> None: + @ctf_updates_allowed.setter + def ctf_updates_allowed(self, allowed: bool) -> None: self._ctf_updates_allowed = allowed - if allowed and self._prev_cpu_proc is not None: + if allowed and self._prev_proc_data is not None: # Rebroadcast last processor record - message_queue.put_nowait(self._prev_cpu_proc) + message_queue.put_nowait(self._prev_proc_data) + @property def image_updates_allowed(self) -> bool: return self._image_updates_allowed - def set_image_updates_allowed(self, allowed: bool) -> None: + @image_updates_allowed.setter + def image_updates_allowed(self, allowed: bool) -> None: self._image_updates_allowed = allowed if allowed and self._prev_image_array is not None: # Rebroadcast last image record message_queue.put_nowait(self._prev_image_array) + @property def processor_updates_allowed(self) -> bool: return self._processor_updates_allowed - def set_processor_updates_allowed(self, allowed: bool) -> None: + @processor_updates_allowed.setter + def processor_updates_allowed(self, allowed: bool) -> None: self._processor_updates_allowed = allowed - if allowed and self._prev_config is not None: - # Rebroadcast last config record - message_queue.put_nowait(self._prev_config) + if allowed and self._prev_proc_data is not None: + # Rebroadcast last processor record + message_queue.put_nowait(self._prev_proc_data) + @property def shader_updates_allowed(self) -> bool: return self._shader_updates_allowed - def set_shader_updates_allowed(self, allowed: bool) -> None: + @shader_updates_allowed.setter + def shader_updates_allowed(self, allowed: bool) -> None: self._shader_updates_allowed = allowed - if allowed and self._prev_cpu_proc is not None: + if allowed and self._prev_proc_data is not None: # Rebroadcast last processor record - message_queue.put_nowait(self._prev_cpu_proc) + message_queue.put_nowait(self._prev_proc_data) def is_routing(self) -> bool: """Whether runner is routing messages.""" @@ -156,14 +169,19 @@ def start_routing(self) -> None: self._handle_config_message(msg_raw) # OCIO processor - elif isinstance(msg_raw, ocio.Processor): - self._prev_cpu_proc = msg_raw + elif ( + isinstance(msg_raw, tuple) + and len(msg_raw) == 2 + and isinstance(msg_raw[0], ProcessorContext) + and isinstance(msg_raw[1], ocio.Processor) + ): + self._prev_proc_data = msg_raw if ( self._processor_updates_allowed or self._ctf_updates_allowed or self._shader_updates_allowed ): - self._handle_processor_message(msg_raw) + self._handle_processor_message(*msg_raw) # Image array elif isinstance(msg_raw, np.ndarray): @@ -173,7 +191,7 @@ def start_routing(self) -> None: # Python or OCIO log record else: - self._handle_log_message(msg_raw) + self._handle_log_message(str(msg_raw)) self._is_routing = False @@ -190,22 +208,27 @@ def _handle_config_message(self, config: ocio.Config) -> None: # Pass error to log self._handle_log_message(str(e), force_level=self.LOG_LEVEL_WARNING) - def _handle_processor_message(self, cpu_proc: ocio.Processor) -> None: + def _handle_processor_message( + self, + proc_context: ProcessorContext, + proc: ocio.Processor, + ) -> None: """ Handle OCIO processor received in the message queue. - :param cpu_proc: OCIO processor instance + :param proc_context: OCIO processor context data + :param proc: OCIO processor instance """ try: if self._processor_updates_allowed: - self.processor_ready.emit(cpu_proc.getDefaultCPUProcessor()) + self.processor_ready.emit(proc_context, proc.getDefaultCPUProcessor()) if self._ctf_updates_allowed: - ctf_html_data, group_tf = processor_to_ctf_html(cpu_proc) + ctf_html_data, group_tf = processor_to_ctf_html(proc) self.ctf_html_ready.emit(ctf_html_data, group_tf) if self._shader_updates_allowed: - gpu_proc = cpu_proc.getDefaultGPUProcessor() + gpu_proc = proc.getDefaultGPUProcessor() shader_html_data = processor_to_shader_html( gpu_proc, self._gpu_language ) @@ -317,6 +340,54 @@ def __getattr__(self, item: str) -> Any: """Forward unknown attribute requests to internal runner.""" return getattr(self._runner, item) + @property + def gpu_language(self) -> ocio.GpuLanguage: + return self._runner.gpu_language + + @gpu_language.setter + def gpu_language(self, gpu_language: ocio.GpuLanguage) -> None: + self._runner.gpu_language = gpu_language + + @property + def config_updates_allowed(self) -> bool: + return self._runner.config_updates_allowed + + @config_updates_allowed.setter + def config_updates_allowed(self, allowed: bool) -> None: + self._runner.config_updates_allowed = allowed + + @property + def ctf_updates_allowed(self) -> bool: + return self._runner.ctf_updates_allowed + + @ctf_updates_allowed.setter + def ctf_updates_allowed(self, allowed: bool) -> None: + self._runner.ctf_updates_allowed = allowed + + @property + def image_updates_allowed(self) -> bool: + return self._runner.image_updates_allowed + + @image_updates_allowed.setter + def image_updates_allowed(self, allowed: bool) -> None: + self._runner.image_updates_allowed = allowed + + @property + def processor_updates_allowed(self) -> bool: + return self._runner.processor_updates_allowed + + @processor_updates_allowed.setter + def processor_updates_allowed(self, allowed: bool) -> None: + self._runner.processor_updates_allowed = allowed + + @property + def shader_updates_allowed(self) -> bool: + return self._runner.shader_updates_allowed + + @shader_updates_allowed.setter + def shader_updates_allowed(self, allowed: bool) -> None: + self._runner.shader_updates_allowed = allowed + def end_routing(self) -> None: """Stop message routing thread.""" if not self._runner.is_routing(): diff --git a/src/apps/ocioview/ocioview/processor_context.py b/src/apps/ocioview/ocioview/processor_context.py new file mode 100644 index 0000000000..804a649542 --- /dev/null +++ b/src/apps/ocioview/ocioview/processor_context.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Type + +import PyOpenColorIO as ocio + + +@dataclass +class ProcessorContext: + """ + Data about current config items that constructed a processor. + """ + + input_color_space: str | None + """Input color space name.""" + + transform_item_type: Type | None + """Transform source config item type.""" + + transform_item_name: str | None + """Transform source config item name.""" + + transform_direction: ocio.TransformDirection = ocio.TRANSFORM_DIR_FORWARD + """Transform direction being viewed.""" diff --git a/src/apps/ocioview/ocioview/ref_space_manager.py b/src/apps/ocioview/ocioview/ref_space_manager.py index 0ee3353205..3de7ab84dc 100644 --- a/src/apps/ocioview/ocioview/ref_space_manager.py +++ b/src/apps/ocioview/ocioview/ref_space_manager.py @@ -13,6 +13,11 @@ class ReferenceSpaceManager: _ref_display_name: Optional[str] = None _ref_subscribers: list[Callable] = [] + @classmethod + def init_reference_spaces(cls) -> None: + cls._update_scene_reference_space() + cls._update_display_reference_space() + @classmethod def subscribe_to_reference_spaces(cls, ref_callback: Callable) -> None: """ diff --git a/src/apps/ocioview/ocioview/transform_manager.py b/src/apps/ocioview/ocioview/transform_manager.py index 65d72a2487..c1990d2014 100644 --- a/src/apps/ocioview/ocioview/transform_manager.py +++ b/src/apps/ocioview/ocioview/transform_manager.py @@ -1,14 +1,17 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +from __future__ import annotations + from collections import defaultdict from dataclasses import dataclass from functools import partial -from typing import Callable, Optional +from typing import Callable, Optional, Type, Union import PyOpenColorIO as ocio from PySide6 import QtCore, QtGui +from .constants import ICON_SIZE_ITEM from .utils import get_glyph_icon @@ -17,7 +20,7 @@ class TransformSubscription: """Reference for one item transform subscription.""" item_model: QtCore.QAbstractItemModel - item_name: str + item_label: str class TransformAgent(QtCore.QObject): @@ -62,7 +65,7 @@ class TransformManager: @classmethod def set_subscription( - cls, slot: int, item_model: QtCore.QAbstractItemModel, item_name: str + cls, slot: int, item_model: QtCore.QAbstractItemModel, item_label: str ) -> None: """ Set the transform for a specific subscription slot, so that @@ -71,7 +74,7 @@ def set_subscription( :param slot: Subscription slot number between 1-10 :param item_model: Model for item and its transforms - :param item_name: Item name + :param item_label: Item label """ prev_item_model = None @@ -86,14 +89,14 @@ def set_subscription( for other_slot, tf_subscription in list(cls._tf_subscriptions.items()): if ( tf_subscription.item_model == item_model - and tf_subscription.item_name == item_name + and tf_subscription.item_label == item_label ): tf_agent = tf_subscription.item_model.get_transform_agent(slot) tf_agent.disconnect_all() del cls._tf_subscriptions[other_slot] # Connect new subscription - tf_subscription = TransformSubscription(item_model, item_name) + tf_subscription = TransformSubscription(item_model, item_label) tf_agent = item_model.get_transform_agent(slot) tf_agent.item_name_changed.connect(partial(cls._on_item_name_changed, slot)) tf_agent.item_tf_changed.connect(partial(cls._on_item_tf_changed, slot)) @@ -107,7 +110,7 @@ def set_subscription( init_callback(slot) # Trigger immediate update to subscribers of this slot - cls._on_item_tf_changed(slot, *item_model.get_item_transforms(item_name)) + cls._on_item_tf_changed(slot, *item_model.get_item_transforms(item_label)) # Repaint views for previous and new model if prev_item_model is not None: @@ -117,21 +120,21 @@ def set_subscription( @classmethod def get_subscription_slot( - cls, item_model: QtCore.QAbstractItemModel, item_name: str + cls, item_model: QtCore.QAbstractItemModel, item_label: str ) -> int: """ Return the subscription slot number for a transform - with the provided item model and name, if set. + with the provided item model and label, if set. :param item_model: Model for item and its transforms - :param item_name: Item name + :param item_label: Item label :return: Subscription slot number, or -1 if no subscription is set. """ for slot, tf_subscription in cls._tf_subscriptions.items(): if ( tf_subscription.item_model == item_model - and tf_subscription.item_name == item_name + and tf_subscription.item_label == item_label ): return slot return -1 @@ -139,7 +142,7 @@ def get_subscription_slot( @classmethod def get_subscription_slot_color( cls, slot: int, saturation: float = 0.5, value: float = 1.0 - ) -> Optional[QtGui.QColor]: + ) -> Union[QtGui.QColor, None]: """ Return a standard subscription slot color for use in GUI elements. @@ -156,7 +159,7 @@ def get_subscription_slot_color( return None @classmethod - def get_subscription_slot_icon(cls, slot: int) -> Optional[QtGui.QIcon]: + def get_subscription_slot_icon(cls, slot: int) -> Union[QtGui.QIcon, None]: """ Return a standard subscription slot icon for use in GUI elements. @@ -178,18 +181,29 @@ def get_subscription_slot_icon(cls, slot: int) -> Optional[QtGui.QIcon]: 9: "nine", }[slot] color = cls.get_subscription_slot_color(slot) - return get_glyph_icon(f"ph.number-circle-{slot_word}", color=color) + return get_glyph_icon( + f"ph.number-circle-{slot_word}", color=color, size=ICON_SIZE_ITEM + ) else: return None @classmethod - def get_subscription_menu_items(cls) -> list[tuple[int, str, QtGui.QIcon]]: + def get_subscription_menu_items( + cls, + ) -> list[tuple[int, str, Type, str, QtGui.QIcon]]: """ - :return: Subscription slots, their names, and respective item - type icons, for use in subscription menus. + :return: Subscription slots, their labels, associated item + types and names, and slot icons, for use in subscription + menus. """ return [ - (i, s.item_name, cls.get_subscription_slot_icon(i)) + ( + i, + s.item_label, + s.item_model.__item_type__, + s.item_model.extract_subscription_item_name(s.item_label), + cls.get_subscription_slot_icon(i), + ) for i, s in sorted(cls._tf_subscriptions.items()) ] @@ -245,7 +259,7 @@ def subscribe_to_transforms_at(cls, slot: int, tf_callback: Callable) -> None: tf_callback( slot, *tf_subscription.item_model.get_item_transforms( - tf_subscription.item_name + tf_subscription.item_label ), ) @@ -283,16 +297,16 @@ def _update_menu_items(cls) -> None: callback(menu_items) @classmethod - def _on_item_name_changed(cls, slot: int, item_name: str) -> None: + def _on_item_name_changed(cls, slot: int, item_label: str) -> None: """ Called when a subscription item is renamed, for internal and subscriber tracking. """ tf_subscription = cls._tf_subscriptions.get(slot) if tf_subscription is not None: - tf_subscription.item_name = item_name + tf_subscription.item_label = item_label cls._on_item_tf_changed( - slot, *tf_subscription.item_model.get_item_transforms(item_name) + slot, *tf_subscription.item_model.get_item_transforms(item_label) ) cls._update_menu_items() diff --git a/src/apps/ocioview/ocioview/transforms/transform_edit.py b/src/apps/ocioview/ocioview/transforms/transform_edit.py index 9c297fa2a6..c517a71d86 100644 --- a/src/apps/ocioview/ocioview/transforms/transform_edit.py +++ b/src/apps/ocioview/ocioview/transforms/transform_edit.py @@ -45,7 +45,7 @@ def transform_type_icon(cls) -> QtGui.QIcon: :return: Transform type icon """ if cls.__icon__ is None: - cls.__icon__ = get_glyph_icon(cls.__icon_glyph__) + cls.__icon__ = get_glyph_icon(cls.__icon_glyph__, size=ICON_SIZE_ITEM) return cls.__icon__ @classmethod diff --git a/src/apps/ocioview/ocioview/utils.py b/src/apps/ocioview/ocioview/utils.py index f4a975b1f1..274686ce4b 100644 --- a/src/apps/ocioview/ocioview/utils.py +++ b/src/apps/ocioview/ocioview/utils.py @@ -42,6 +42,7 @@ def get_glyph_icon( scale_factor: float = ICON_SCALE_FACTOR, color: Optional[QtGui.QColor] = None, as_widget: bool = False, + size: QtCore.QSize = ICON_SIZE_BUTTON, ) -> Union[QtGui.QIcon, QtWidgets.QLabel]: """ Get named glyph QIcon from QtAwesome. @@ -51,6 +52,7 @@ def get_glyph_icon( :param color: Optional icon color override :param as_widget: Set to True to return a widget displaying the icon instead of a QIcon. + :param size: Override icon size :return: Glyph QIcon or QLabel """ kwargs = {"scale_factor": scale_factor} @@ -61,7 +63,7 @@ def get_glyph_icon( if as_widget: widget = QtWidgets.QLabel() - widget.setPixmap(icon.pixmap(ICON_SIZE_BUTTON)) + widget.setPixmap(icon.pixmap(size)) return widget else: return icon diff --git a/src/apps/ocioview/ocioview/viewer/image_plane.py b/src/apps/ocioview/ocioview/viewer/image_plane.py index f702529239..7ceb11b5af 100644 --- a/src/apps/ocioview/ocioview/viewer/image_plane.py +++ b/src/apps/ocioview/ocioview/viewer/image_plane.py @@ -5,6 +5,8 @@ # oglapphelpers library bundled with OCIO. We should fully # reimplement that in Python for direct use in applications. +from __future__ import annotations + import ctypes import logging import math @@ -19,6 +21,7 @@ from PySide6 import QtCore, QtGui, QtWidgets, QtOpenGLWidgets from ..log_handlers import message_queue +from ..processor_context import ProcessorContext from ..ref_space_manager import ReferenceSpaceManager from .utils import load_image @@ -111,13 +114,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self._gl_ready = False # Color management - self._ocio_input_color_space = None self._ocio_tf = None self._ocio_exposure = 0.0 self._ocio_gamma = 1.0 self._ocio_channel_hot = [1, 1, 1, 1] - self._ocio_tf_proc = None - self._ocio_tf_proc_cpu = None + self._ocio_proc_context = ProcessorContext(None, None, None) + self._ocio_proc = None + self._ocio_proc_cpu = None self._ocio_proc_cache_id = None self._ocio_shader_cache_id = None self._ocio_shader_desc = None @@ -339,11 +342,20 @@ def load_image(self, image_path: Path) -> None: ) if not color_space_name: # Use previous or config default - if self._ocio_input_color_space: - color_space_name = self._ocio_input_color_space + if self._ocio_proc_context: + color_space_name = self._ocio_proc_context.input_color_space else: color_space_name = ocio.ROLE_DEFAULT - self._ocio_input_color_space = color_space_name + + if self._ocio_proc_context: + proc_context = ProcessorContext( + color_space_name, + self._ocio_proc_context.transform_item_type, + self._ocio_proc_context.transform_item_name, + self._ocio_proc_context.direction, + ) + else: + proc_context = ProcessorContext(color_space_name, None, None) # Load image data via an available image library self._image_array = load_image(image_path) @@ -375,7 +387,7 @@ def load_image(self, image_path: Path) -> None: image_path, int(self._image_size[0]), int(self._image_size[1]) ) - self.update_ocio_proc(input_color_space=self._ocio_input_color_space) + self.update_ocio_proc(proc_context=proc_context) self.fit() # Log image change after load and render @@ -389,11 +401,14 @@ def broadcast_image(self) -> None: if self._image_array is not None: message_queue.put_nowait(self._image_array) - def input_color_space(self) -> str: + def input_color_space(self) -> str | None: """ :return: Current input OCIO color space name """ - return self._ocio_input_color_space + if self._ocio_proc_context: + return self._ocio_proc_context.input_color_space + else: + return None def transform(self) -> Optional[ocio.Transform]: """ @@ -415,7 +430,7 @@ def reset_ocio_proc(self, update: bool = False) -> None: :param update: Whether to redraw viewport """ - self._ocio_input_color_space = None + self._ocio_proc_context = None self._ocio_tf = None self._ocio_exposure = 0.0 self._ocio_gamma = 1.0 @@ -426,7 +441,7 @@ def reset_ocio_proc(self, update: bool = False) -> None: def update_ocio_proc( self, - input_color_space: Optional[str] = None, + proc_context: Optional[ProcessorContext] = None, transform: Optional[ocio.Transform] = None, channel: Optional[int] = None, force_update: bool = False, @@ -437,7 +452,7 @@ def update_ocio_proc( state. This will trigger a GL update IF the underlying OCIO ops in the processor have changed. - :param input_color_space: Input OCIO color space name + :param proc_context: Processor context data :param transform: Optional main OCIO transform, to be applied from the current config's scene reference space. :param channel: ImagePlaneChannels value to toggle channel @@ -446,8 +461,8 @@ def update_ocio_proc( when the processor has not been updated. """ # Update processor parameters - if input_color_space is not None: - self._ocio_input_color_space = input_color_space + if proc_context is not None: + self._ocio_proc_context = proc_context if transform is not None: self._ocio_tf = transform if channel is not None: @@ -464,9 +479,10 @@ def update_ocio_proc( cpu_viewing_pipeline = ocio.GroupTransform() # Convert to scene linear space if input space is known - if has_scene_linear and self._ocio_input_color_space: + if has_scene_linear and self._ocio_proc_context: to_scene_linear = ocio.ColorSpaceTransform( - src=self._ocio_input_color_space, dst=ocio.ROLE_SCENE_LINEAR + src=self._ocio_proc_context.input_color_space, + dst=ocio.ROLE_SCENE_LINEAR, ) gpu_viewing_pipeline.appendTransform(to_scene_linear) cpu_viewing_pipeline.appendTransform(to_scene_linear) @@ -481,18 +497,19 @@ def update_ocio_proc( # Convert to the scene reference space, which is the expected input space for # all provided transforms. If the input color space is not known, the transform # will be applied to unmodified input pixels. - if has_scene_linear and self._ocio_input_color_space: - to_scene_ref = ocio.ColorSpaceTransform( - src=ocio.ROLE_SCENE_LINEAR, dst=scene_ref_name - ) - gpu_viewing_pipeline.appendTransform(to_scene_ref) - cpu_viewing_pipeline.appendTransform(to_scene_ref) - elif self._ocio_input_color_space: - to_scene_ref = ocio.ColorSpaceTransform( - src=self._ocio_input_color_space, dst=scene_ref_name - ) - gpu_viewing_pipeline.appendTransform(to_scene_ref) - cpu_viewing_pipeline.appendTransform(to_scene_ref) + if self._ocio_proc_context and self._ocio_proc_context.input_color_space: + if has_scene_linear: + to_scene_ref = ocio.ColorSpaceTransform( + src=ocio.ROLE_SCENE_LINEAR, dst=scene_ref_name + ) + gpu_viewing_pipeline.appendTransform(to_scene_ref) + cpu_viewing_pipeline.appendTransform(to_scene_ref) + else: + to_scene_ref = ocio.ColorSpaceTransform( + src=self._ocio_proc_context.input_color_space, dst=scene_ref_name + ) + gpu_viewing_pipeline.appendTransform(to_scene_ref) + cpu_viewing_pipeline.appendTransform(to_scene_ref) # Main transform if self._ocio_tf is not None: @@ -500,9 +517,9 @@ def update_ocio_proc( cpu_viewing_pipeline.appendTransform(self._ocio_tf) # Or restore input color space, if known - elif self._ocio_input_color_space: + elif self._ocio_proc_context and self._ocio_proc_context.input_color_space: from_scene_ref = ocio.ColorSpaceTransform( - src=scene_ref_name, dst=self._ocio_input_color_space + src=scene_ref_name, dst=self._ocio_proc_context.input_color_space ) gpu_viewing_pipeline.appendTransform(from_scene_ref) cpu_viewing_pipeline.appendTransform(from_scene_ref) @@ -530,8 +547,8 @@ def update_ocio_proc( cpu_proc = config.getProcessor( cpu_viewing_pipeline, ocio.TRANSFORM_DIR_FORWARD ) - self._ocio_tf_proc = cpu_proc - self._ocio_tf_proc_cpu = cpu_proc.getDefaultCPUProcessor() + self._ocio_proc = cpu_proc + self._ocio_proc_cpu = cpu_proc.getDefaultCPUProcessor() # Update GPU processor shaders and textures self._ocio_shader_desc = ocio.GpuShaderDesc.CreateShaderDesc( @@ -553,16 +570,16 @@ def update_ocio_proc( self.update() # Log processor change after render - message_queue.put_nowait(cpu_proc) + message_queue.put_nowait((self._ocio_proc_context, self._ocio_proc)) elif force_update: self.update() # The transform and processor has not changed, but other app components - # which view it may have dropped tje reference. Log processor to update + # which view it may have dropped the reference. Log processor to update # them as needed. - if self._ocio_tf_proc is not None: - message_queue.put_nowait(self._ocio_tf_proc) + if self._ocio_proc is not None and self._ocio_proc_context is not None: + message_queue.put_nowait((self._ocio_proc_context, self._ocio_proc)) def exposure(self) -> float: """ @@ -663,8 +680,8 @@ def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: pixel_input = pixel_input[:3] # Sample output pixel with CPU processor - if self._ocio_tf_proc_cpu is not None: - pixel_output = self._ocio_tf_proc_cpu.applyRGB(pixel_input) + if self._ocio_proc_cpu is not None: + pixel_output = self._ocio_proc_cpu.applyRGB(pixel_input) else: pixel_output = pixel_input.copy() diff --git a/src/apps/ocioview/ocioview/viewer/image_viewer.py b/src/apps/ocioview/ocioview/viewer/image_viewer.py index f46ec71813..ca0fde2198 100644 --- a/src/apps/ocioview/ocioview/viewer/image_viewer.py +++ b/src/apps/ocioview/ocioview/viewer/image_viewer.py @@ -1,16 +1,19 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +from __future__ import annotations + from contextlib import contextmanager from pathlib import Path -from typing import Generator, Optional +from typing import Generator, Optional, Type import PyOpenColorIO as ocio from PySide6 import QtCore, QtGui, QtWidgets +from ..processor_context import ProcessorContext from ..transform_manager import TransformManager from ..config_cache import ConfigCache -from ..constants import GRAY_COLOR, R_COLOR, G_COLOR, B_COLOR +from ..constants import GRAY_COLOR, R_COLOR, G_COLOR, B_COLOR, ICON_SIZE_TAB from ..utils import float_to_uint8, get_glyph_icon, SignalsBlocked from ..widgets import ComboBox, CallbackComboBox from .image_plane import ImagePlane @@ -43,12 +46,16 @@ class ImageViewer(QtWidgets.QWidget): WIDGET_HEIGHT_IO = 32 + ROLE_SLOT = QtCore.Qt.UserRole + 1 + ROLE_ITEM_TYPE = QtCore.Qt.UserRole + 2 + ROLE_ITEM_NAME = QtCore.Qt.UserRole + 3 + @classmethod def viewer_type_icon(cls) -> QtGui.QIcon: """ :return: Viewer type icon """ - return get_glyph_icon("mdi6.image-outline") + return get_glyph_icon("mdi6.image-outline", size=ICON_SIZE_TAB) @classmethod def viewer_type_label(cls) -> str: @@ -284,7 +291,13 @@ def update(self, force: bool = False) -> None: self._update_input_color_spaces(update=False) self.image_plane.update_ocio_proc( - input_color_space=self.input_color_space(), force_update=force + proc_context=ProcessorContext( + self.input_color_space(), + self.transform_item_type(), + self.transform_item_name(), + self.transform_direction(), + ), + force_update=force, ) super().update() @@ -382,9 +395,15 @@ def set_transform( self._tf_inv = transform_inv self.image_plane.update_ocio_proc( + proc_context=ProcessorContext( + self.input_color_space(), + self.transform_item_type(), + self.transform_item_name(), + tf_direction, + ), transform=self._tf_inv if tf_direction == ocio.TRANSFORM_DIR_INVERSE - else self._tf_fwd + else self._tf_fwd, ) def clear_transform(self) -> None: @@ -401,6 +420,18 @@ def clear_transform(self) -> None: self.image_plane.clear_transform() + def transform_item_type(self) -> Type | None: + """ + :return: Transform source config item type + """ + return self.tf_box.currentData(role=self.ROLE_ITEM_TYPE) + + def transform_item_name(self) -> str | None: + """ + :return: Transform source config item name + """ + return self.tf_box.currentData(role=self.ROLE_ITEM_NAME) + def transform_direction(self) -> ocio.TransformDirection: """ :return: Transform direction being viewed @@ -495,18 +526,25 @@ def _on_transform_menu_changed( target_index = -1 current_slot = -1 if self.tf_box.count(): - current_slot = self.tf_box.currentData() + current_slot = self.tf_box.currentData(role=self.ROLE_SLOT) with SignalsBlocked(self.tf_box): self.tf_box.clear() # The first item is always no transform - self.tf_box.addItem(self.PASSTHROUGH, userData=-1) - - for i, (slot, item_name, item_type_icon) in enumerate(menu_items): - self.tf_box.addItem(item_type_icon, item_name, userData=slot) + self.tf_box.addItem(self.PASSTHROUGH) + self.tf_box.setItemData(0, -1, role=self.ROLE_SLOT) + + for i, (slot, item_label, item_type, item_name, slot_icon) in enumerate( + menu_items + ): + index = i + 1 + self.tf_box.addItem(slot_icon, item_label) + self.tf_box.setItemData(index, slot, role=self.ROLE_SLOT) + self.tf_box.setItemData(index, item_type, role=self.ROLE_ITEM_TYPE) + self.tf_box.setItemData(index, item_name, role=self.ROLE_ITEM_NAME) if slot == current_slot: - target_index = i + 1 # Offset for "Passthrough" item + target_index = index # Offset for "Passthrough" item # Restore previous item? if target_index != -1: @@ -529,7 +567,7 @@ def _on_transform_subscription_init(self, slot: int) -> None: :param slot: Transform subscription slot """ if self._tf_subscription_slot == -1: - index = self.tf_box.findData(slot) + index = self.tf_box.findData(slot, role=self.ROLE_SLOT) if index != -1: self.tf_box.setCurrentIndex(index) @@ -539,7 +577,7 @@ def _on_transform_changed(self, index: int) -> None: TransformManager.unsubscribe_from_all_transforms(self.set_transform) self.clear_transform() else: - self._tf_subscription_slot = self.tf_box.currentData() + self._tf_subscription_slot = self.tf_box.currentData(role=self.ROLE_SLOT) TransformManager.subscribe_to_transforms_at( self._tf_subscription_slot, self.set_transform ) @@ -548,7 +586,9 @@ def _on_transform_changed(self, index: int) -> None: def _on_tf_subscription_requested(self, slot: int) -> None: # If the requested slot does not have a subscription, "Passthrough" will # be selected. - self.tf_box.setCurrentIndex(max(0, self.tf_box.findData(slot))) + self.tf_box.setCurrentIndex( + max(0, self.tf_box.findData(slot, role=self.ROLE_SLOT)) + ) @QtCore.Slot(bool) def _on_inverse_check_clicked(self, checked: bool) -> None: @@ -631,7 +671,14 @@ def _on_sample_changed( @QtCore.Slot(str) def _on_input_color_space_changed(self, input_color_space: str) -> None: - self.image_plane.update_ocio_proc(input_color_space=input_color_space) + self.image_plane.update_ocio_proc( + proc_context=ProcessorContext( + input_color_space, + self.transform_item_type(), + self.transform_item_name(), + self.transform_direction(), + ) + ) @QtCore.Slot(float) def _on_exposure_changed(self, value: float) -> None: diff --git a/src/apps/ocioview/ocioview/widgets/structure.py b/src/apps/ocioview/ocioview/widgets/structure.py index d248f88d88..f5fe7d3054 100644 --- a/src/apps/ocioview/ocioview/widgets/structure.py +++ b/src/apps/ocioview/ocioview/widgets/structure.py @@ -6,7 +6,7 @@ from PySide6 import QtCore, QtGui, QtWidgets -from ..constants import ICON_SIZE_BUTTON, ICON_SIZE_ITEM, BORDER_COLOR_ROLE +from ..constants import ICON_SIZE_ITEM, ICON_SIZE_TAB, BORDER_COLOR_ROLE from ..style import apply_top_tool_bar_style from ..utils import get_icon @@ -82,6 +82,7 @@ def __init__( # Widgets self.tabs = QtWidgets.QTabWidget() + self.tabs.setIconSize(ICON_SIZE_TAB) self.setWidget(self.tabs) # Connections @@ -137,8 +138,8 @@ def _rotate_icon( xform = QtGui.QTransform() xform.rotate(icon_rot) - pixmap = icon.pixmap(ICON_SIZE_BUTTON) - pixmap = pixmap.transformed(xform) + pixmap = icon.pixmap(ICON_SIZE_TAB) + pixmap = pixmap.transformed(xform, QtCore.Qt.SmoothTransformation) return QtGui.QIcon(pixmap)