diff --git a/src/apps/ocioview/main.py b/src/apps/ocioview/main.py
index 2c2b1bffde..15d897ab5d 100644
--- a/src/apps/ocioview/main.py
+++ b/src/apps/ocioview/main.py
@@ -1,68 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright Contributors to the OpenColorIO Project.
-import logging
-import os
import sys
-from pathlib import Path
-import PyOpenColorIO as ocio
-from PySide6 import QtCore, QtGui, QtWidgets
-
-import ocioview.log_handlers # Import to initialize logging
from ocioview.main_window import OCIOView
-from ocioview.style import QSS, DarkPalette
-
-
-ROOT_DIR = Path(__file__).resolve().parent.parent
-FONTS_DIR = ROOT_DIR / "fonts"
-
-
-def excepthook(exc_type, exc_value, exc_tb):
- """Log uncaught errors"""
- if issubclass(exc_type, KeyboardInterrupt):
- sys.__excepthook__(exc_type, exc_value, exc_tb)
- return
- logging.error(f"{exc_value}", exc_info=exc_value)
+from ocioview.setup import setup_app
if __name__ == "__main__":
- sys.excepthook = excepthook
-
- # OpenGL core profile needed on macOS to access programmatic pipeline
- gl_format = QtGui.QSurfaceFormat()
- gl_format.setProfile(QtGui.QSurfaceFormat.CoreProfile)
- gl_format.setSwapInterval(1)
- gl_format.setVersion(4, 0)
- QtGui.QSurfaceFormat.setDefaultFormat(gl_format)
-
- # Create app
- app = QtWidgets.QApplication(sys.argv)
-
- # Initialize style
- app.setStyle("fusion")
- app.setPalette(DarkPalette())
- app.setStyleSheet(QSS)
- app.setEffectEnabled(QtCore.Qt.UI_AnimateCombo, False)
-
- font = app.font()
- font.setPointSize(8)
- app.setFont(font)
-
- # Clean OCIO environment to isolate working config
- for env_var in (
- ocio.OCIO_CONFIG_ENVVAR,
- ocio.OCIO_ACTIVE_VIEWS_ENVVAR,
- ocio.OCIO_ACTIVE_DISPLAYS_ENVVAR,
- ocio.OCIO_INACTIVE_COLORSPACES_ENVVAR,
- ocio.OCIO_OPTIMIZATION_FLAGS_ENVVAR,
- ocio.OCIO_USER_CATEGORIES_ENVVAR,
- ):
- if env_var in os.environ:
- del os.environ[env_var]
+ app = setup_app()
# Start ocioview
- ocioview = OCIOView()
- ocioview.show()
+ ocio_view = OCIOView()
+ ocio_view.show()
sys.exit(app.exec_())
diff --git a/src/apps/ocioview/ocioview/config_cache.py b/src/apps/ocioview/ocioview/config_cache.py
index 63eaaeeaff..280442ca97 100644
--- a/src/apps/ocioview/ocioview/config_cache.py
+++ b/src/apps/ocioview/ocioview/config_cache.py
@@ -21,7 +21,10 @@ class ConfigCache:
_active_views: Optional[list[str]] = None
_all_names: Optional[list[str]] = None
_categories: Optional[list[str]] = None
- _color_spaces: dict[bool, list[ocio.ColorSpace]] = {}
+ _color_spaces: dict[
+ tuple[bool, ocio.SearchReferenceSpaceType, ocio.ColorSpaceVisibility],
+ Union[list[ocio.ColorSpace], ocio.ColorSpaceSet],
+ ] = {}
_color_space_names: dict[ocio.SearchReferenceSpaceType, list[str]] = {}
_default_color_space_name: Optional[str] = None
_default_view_transform_name: Optional[str] = None
@@ -117,7 +120,10 @@ def get_active_displays(cls) -> list[str]:
cls._active_displays = list(
filter(
None,
- re.split(r" *[,:] *", ocio.GetCurrentConfig().getActiveDisplays()),
+ re.split(
+ r" *[,:] *",
+ ocio.GetCurrentConfig().getActiveDisplays(),
+ ),
)
)
@@ -132,7 +138,9 @@ def get_active_views(cls) -> list[str]:
cls._active_views = list(
filter(
None,
- re.split(r" *[,:] *", ocio.GetCurrentConfig().getActiveViews()),
+ re.split(
+ r" *[,:] *", ocio.GetCurrentConfig().getActiveViews()
+ ),
)
)
@@ -213,23 +221,34 @@ def get_categories(cls) -> list[str]:
@classmethod
def get_color_spaces(
- cls, as_set: bool = False
+ cls,
+ reference_space_type: Optional[ocio.SearchReferenceSpaceType] = None,
+ visibility: Optional[ocio.ColorSpaceVisibility] = None,
+ as_set: bool = False,
) -> Union[list[ocio.ColorSpace], ocio.ColorSpaceSet]:
"""
Get all (all reference space types and visibility states) color
spaces from the current config.
+ :param reference_space_type: Optionally filter by reference
+ space type.
+ :param visibility: Optional filter by visibility
:param as_set: If True, put returned color spaces into a
ColorSpaceSet, which copies the spaces to insulate from config
changes.
:return: list or color space set of color spaces
"""
- cache_key = as_set
+ if reference_space_type is None:
+ reference_space_type = ocio.SEARCH_REFERENCE_SPACE_ALL
+ if visibility is None:
+ visibility = ocio.COLORSPACE_ALL
+
+ cache_key = (as_set, reference_space_type, visibility)
if not cls.validate() or cache_key not in cls._color_spaces:
config = ocio.GetCurrentConfig()
color_spaces = config.getColorSpaces(
- ocio.SEARCH_REFERENCE_SPACE_ALL, ocio.COLORSPACE_ALL
+ reference_space_type, visibility
)
if as_set:
color_space_set = ocio.ColorSpaceSet()
@@ -253,7 +272,10 @@ def get_color_space_names(
"""
cache_key = reference_space_type
- if not cls.validate() or reference_space_type not in cls._color_space_names:
+ if (
+ not cls.validate()
+ or reference_space_type not in cls._color_space_names
+ ):
cls._color_space_names[cache_key] = list(
ocio.GetCurrentConfig().getColorSpaceNames(
reference_space_type, ocio.COLORSPACE_ALL
@@ -402,7 +424,9 @@ def get_named_transforms(cls) -> list[ocio.NamedTransform]:
"""
if not cls.validate() or cls._named_transforms is None:
cls._named_transforms = list(
- ocio.GetCurrentConfig().getNamedTransforms(ocio.NAMEDTRANSFORM_ALL)
+ ocio.GetCurrentConfig().getNamedTransforms(
+ ocio.NAMEDTRANSFORM_ALL
+ )
)
return cls._named_transforms
@@ -471,7 +495,9 @@ def get_view_transforms(cls) -> list[ocio.ViewTransform]:
:return: List of view transforms from the current config
"""
if not cls.validate() or cls._view_transforms is None:
- cls._view_transforms = list(ocio.GetCurrentConfig().getViewTransforms())
+ cls._view_transforms = list(
+ ocio.GetCurrentConfig().getViewTransforms()
+ )
return cls._view_transforms
@@ -496,7 +522,10 @@ def get_viewing_rule_names(cls) -> list[str]:
if not cls.validate() or cls._viewing_rule_names is None:
viewing_rules = ocio.GetCurrentConfig().getViewingRules()
cls._viewing_rule_names = sorted(
- [viewing_rules.getName(i) for i in range(viewing_rules.getNumEntries())]
+ [
+ viewing_rules.getName(i)
+ for i in range(viewing_rules.getNumEntries())
+ ]
)
return cls._viewing_rule_names
diff --git a/src/apps/ocioview/ocioview/config_dock.py b/src/apps/ocioview/ocioview/config_dock.py
index 33660b112e..b1c8ab4c4c 100644
--- a/src/apps/ocioview/ocioview/config_dock.py
+++ b/src/apps/ocioview/ocioview/config_dock.py
@@ -6,6 +6,7 @@
import PyOpenColorIO as ocio
from PySide6 import QtCore, QtWidgets
+from .signal_router import SignalRouter
from .items import (
ColorSpaceEdit,
ConfigPropertiesEdit,
@@ -26,10 +27,21 @@ class ConfigDock(TabbedDockWidget):
Dockable widget for editing the current config.
"""
- config_changed = QtCore.Signal()
-
- def __init__(self, parent: Optional[QtCore.QObject] = None):
- super().__init__("Config", get_glyph_icon("ph.file-text"), parent=parent)
+ def __init__(
+ self,
+ corner_widget: Optional[QtWidgets.QWidget] = None,
+ parent: Optional[QtCore.QObject] = None,
+ ):
+ """
+ :param corner_widget: Optional widget to place on the right
+ side of the dock title bar.
+ """
+ super().__init__(
+ "Config",
+ get_glyph_icon("ph.file-text"),
+ corner_widget=corner_widget,
+ parent=parent,
+ )
self._models = []
@@ -50,9 +62,13 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self._connect_config_item_model(self.rule_edit.viewing_rule_edit.model)
self.display_view_edit = DisplayViewEdit()
- self._connect_config_item_model(self.display_view_edit.view_edit.display_model)
+ self._connect_config_item_model(
+ self.display_view_edit.view_edit.display_model
+ )
self._connect_config_item_model(self.display_view_edit.view_edit.model)
- self._connect_config_item_model(self.display_view_edit.shared_view_edit.model)
+ self._connect_config_item_model(
+ self.display_view_edit.shared_view_edit.model
+ )
self._connect_config_item_model(
self.display_view_edit.active_display_view_edit.active_display_edit.model
)
@@ -137,7 +153,9 @@ def update_config_views(self) -> None:
"""
message_queue.put_nowait(ocio.GetCurrentConfig())
- def _connect_config_item_model(self, model: QtCore.QAbstractItemModel) -> None:
+ def _connect_config_item_model(
+ self, model: QtCore.QAbstractItemModel
+ ) -> None:
"""
Collect model and route all config changes to the
'config_changed' signal.
@@ -154,7 +172,7 @@ def _on_config_changed(self, *args, **kwargs) -> None:
"""
Broadcast to the wider application that the config has changed.
"""
- self.config_changed.emit()
+ SignalRouter.get_instance().emit_config_changed()
self.update_config_views()
def _on_warning_raised(self, message: str) -> None:
diff --git a/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py b/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py
index 7516ea2fcc..f030cd0009 100644
--- a/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py
+++ b/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py
@@ -371,9 +371,9 @@ def _setup_visuals(self) -> None:
)
self._visuals["rgb_color_space_input_3d"].visible = False
self._visuals["rgb_color_space_chromaticities_2d"].visible = False
- self._visuals["rgb_color_space_chromaticities_2d"].local.position = (
- np.array([0, 0, 0.00005])
- )
+ self._visuals[
+ "rgb_color_space_chromaticities_2d"
+ ].local.position = np.array([0, 0, 0.00005])
self._visuals["rgb_color_space_chromaticities_3d"].visible = False
self._visuals["rgb_scatter_3d"].visible = False
@@ -494,9 +494,11 @@ def _update_visuals(self, *args):
conversion_chain = []
image_array = np.copy(self._image_array)
+ # Don't try to process single or zero pixel images
+ image_empty = image_array.size <= 3
# 1. Apply current active processor
- if self._processor is not None:
+ if not image_empty and self._processor is not None:
if self._context.transform_item_name is not None:
conversion_chain += [
self._context.input_color_space,
@@ -508,12 +510,12 @@ def _update_visuals(self, *args):
)
if rgb_colourspace is not None:
- self._visuals["rgb_color_space_input_2d"].colourspace = (
- rgb_colourspace
- )
- self._visuals["rgb_color_space_input_3d"].colourspace = (
- rgb_colourspace
- )
+ self._visuals[
+ "rgb_color_space_input_2d"
+ ].colourspace = rgb_colourspace
+ self._visuals[
+ "rgb_color_space_input_3d"
+ ].colourspace = rgb_colourspace
self._processor.applyRGB(image_array)
# 2. Convert from chromaticities input space to "CIE-XYZ-D65" interchange
@@ -559,11 +561,12 @@ def _update_visuals(self, *args):
# 3. Convert from "CIE-XYZ-D65" to "VisualRGBScatter3D" working space
conversion_chain += ["CIE-XYZ-D65", self._working_space]
- image_array = XYZ_to_RGB(
- image_array,
- self._working_space,
- illuminant=self._working_whitepoint,
- )
+ if not image_empty:
+ image_array = XYZ_to_RGB(
+ image_array,
+ self._working_space,
+ illuminant=self._working_whitepoint,
+ )
conversion_chain = [
color_space for color_space, _group in groupby(conversion_chain)
diff --git a/src/apps/ocioview/ocioview/inspect/code_inspector.py b/src/apps/ocioview/ocioview/inspect/code_inspector.py
index 0c8860672f..1e932c2db7 100644
--- a/src/apps/ocioview/ocioview/inspect/code_inspector.py
+++ b/src/apps/ocioview/ocioview/inspect/code_inspector.py
@@ -55,7 +55,9 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.export_button = QtWidgets.QToolButton()
self.export_button.setIcon(get_glyph_icon("mdi6.file-export-outline"))
self.export_button.setText("Export CTF")
- self.export_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+ self.export_button.setToolButtonStyle(
+ QtCore.Qt.ToolButtonTextBesideIcon
+ )
self.export_button.released.connect(self._on_export_button_released)
self.ctf_view = LogView()
@@ -63,8 +65,12 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.ctf_view.append_tool_bar_widget(self.export_button)
self.gpu_language_box = EnumComboBox(ocio.GpuLanguage)
- self.gpu_language_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
- self.gpu_language_box.set_member(MessageRouter.get_instance().gpu_language)
+ self.gpu_language_box.setSizeAdjustPolicy(
+ QtWidgets.QComboBox.AdjustToContents
+ )
+ self.gpu_language_box.set_member(
+ MessageRouter.get_instance().gpu_language
+ )
self.gpu_language_box.currentIndexChanged[int].connect(
self._on_gpu_language_changed
)
@@ -136,7 +142,9 @@ def _scroll_preserved(self, log_view: LogView) -> None:
h_scroll_bar = log_view.horizontalScrollBar()
# Get line number from bottom of view
- prev_cursor = log_view.cursorForPosition(log_view.html_view.rect().bottomLeft())
+ prev_cursor = log_view.cursorForPosition(
+ log_view.html_view.rect().bottomLeft()
+ )
prev_line_num = prev_cursor.blockNumber()
# Get scroll bar positions
@@ -149,7 +157,9 @@ def _scroll_preserved(self, log_view: LogView) -> None:
# Restore current line number
cursor = QtGui.QTextCursor(log_view.document())
cursor.movePosition(
- QtGui.QTextCursor.Down, QtGui.QTextCursor.MoveAnchor, prev_line_num - 1
+ QtGui.QTextCursor.Down,
+ QtGui.QTextCursor.MoveAnchor,
+ prev_line_num - 1,
)
log_view.setTextCursor(cursor)
@@ -167,7 +177,9 @@ def _on_config_html_ready(self, record: str) -> None:
self.config_view.setHtml(record)
@QtCore.Slot(str, ocio.GroupTransform)
- def _on_ctf_html_ready(self, record: str, group_tf: ocio.GroupTransform) -> None:
+ def _on_ctf_html_ready(
+ self, record: str, group_tf: ocio.GroupTransform
+ ) -> None:
"""
Update CTF view with a lossless XML representation of an
OCIO processor.
@@ -178,7 +190,9 @@ def _on_ctf_html_ready(self, record: str, group_tf: ocio.GroupTransform) -> None
self.ctf_view.setHtml(record)
@QtCore.Slot(str, ocio.GPUProcessor)
- def _on_shader_html_ready(self, record: str, gpu_proc: ocio.GPUProcessor) -> None:
+ def _on_shader_html_ready(
+ self, record: str, gpu_proc: ocio.GPUProcessor
+ ) -> None:
"""
Update shader view with fragment shader source created
from an OCIO GPU processor.
diff --git a/src/apps/ocioview/ocioview/inspect/curve_inspector.py b/src/apps/ocioview/ocioview/inspect/curve_inspector.py
index b9013cd3a4..e5298cc1d4 100644
--- a/src/apps/ocioview/ocioview/inspect/curve_inspector.py
+++ b/src/apps/ocioview/ocioview/inspect/curve_inspector.py
@@ -40,7 +40,9 @@ def label(cls) -> str:
@classmethod
def icon(cls) -> QtGui.QIcon:
- return get_glyph_icon("mdi6.chart-bell-curve-cumulative", size=ICON_SIZE_TAB)
+ 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)
@@ -50,18 +52,31 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.input_range_label.setToolTip("Input range")
self.input_range_edit = FloatEditArray(
labels=["min", "max"],
- defaults=[CurveView.INPUT_MIN_DEFAULT, CurveView.INPUT_MAX_DEFAULT],
+ defaults=[
+ CurveView.INPUT_MIN_DEFAULT,
+ CurveView.INPUT_MAX_DEFAULT,
+ ],
)
self.input_range_edit.setToolTip(self.input_range_label.toolTip())
- self.input_range_edit.value_changed.connect(self._on_input_range_changed)
+ self.input_range_edit.value_changed.connect(
+ self._on_input_range_changed
+ )
- self.sample_count_label = get_glyph_icon("ph.line-segments", as_widget=True)
+ self.sample_count_label = get_glyph_icon(
+ "ph.line-segments", as_widget=True
+ )
self.sample_count_label.setToolTip("Sample count")
- self.sample_count_edit = IntEdit(default=CurveView.SAMPLE_COUNT_DEFAULT)
+ self.sample_count_edit = IntEdit(
+ default=CurveView.SAMPLE_COUNT_DEFAULT
+ )
self.sample_count_edit.setToolTip(self.sample_count_label.toolTip())
- self.sample_count_edit.value_changed.connect(self._on_sample_count_changed)
+ self.sample_count_edit.value_changed.connect(
+ self._on_sample_count_changed
+ )
- self.sample_type_label = get_glyph_icon("mdi6.function-variant", as_widget=True)
+ self.sample_type_label = get_glyph_icon(
+ "mdi6.function-variant", as_widget=True
+ )
self.sample_type_label.setToolTip("Sample type")
self.sample_type_combo = EnumComboBox(SampleType)
self.sample_type_combo.setToolTip(self.sample_type_label.toolTip())
@@ -241,6 +256,7 @@ def __init__(
)
# Cached processor from which the OCIO transform is derived
+ self._prev_proc_context = None
self._prev_cpu_proc = None
# Graphics scene
@@ -363,7 +379,10 @@ def set_sample_count(self, sample_count: int) -> None:
:param sample_count: Number of samples. Typically, a power of 2
number.
"""
- if sample_count != self._sample_count and sample_count >= self.SAMPLE_COUNT_MIN:
+ if (
+ sample_count != self._sample_count
+ and sample_count >= self.SAMPLE_COUNT_MIN
+ ):
self._sample_count = sample_count
self._update_curves()
@@ -388,7 +407,9 @@ def set_log_base(self, log_base: int) -> None:
self._log_base = log_base
self._update_curves()
- def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
+ def drawBackground(
+ self, painter: QtGui.QPainter, rect: QtCore.QRectF
+ ) -> None:
"""Draw curve grid and axis values."""
# Flood fill background
painter.setPen(QtCore.Qt.NoPen)
@@ -413,7 +434,9 @@ def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
painter.drawRect(self._curve_rect)
# Draw grid rows
- y_text_origin = QtGui.QTextOption(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ y_text_origin = QtGui.QTextOption(
+ QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
+ )
y_text_origin.setWrapMode(QtGui.QTextOption.NoWrap)
for i, y in enumerate(
@@ -436,12 +459,16 @@ def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
painter.scale(1, -1)
painter.setPen(text_pen)
painter.drawText(
- QtCore.QRectF(-42.5, -10, 40, 20), str(label_value), y_text_origin
+ QtCore.QRectF(-42.5, -10, 40, 20),
+ str(label_value),
+ y_text_origin,
)
painter.restore()
# Draw grid columns
- x_text_origin = QtGui.QTextOption(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ x_text_origin = QtGui.QTextOption(
+ QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
+ )
x_text_origin.setWrapMode(QtGui.QTextOption.NoWrap)
sample_step = math.ceil(self._sample_count / 10.0)
@@ -459,7 +486,9 @@ def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
if x > self._x_min:
label_value = round(
- x if self._sample_type == SampleType.LINEAR else self._x_log[i],
+ x
+ if self._sample_type == SampleType.LINEAR
+ else self._x_log[i],
2 if self._sample_type == SampleType.LINEAR else 5,
)
if label_value == 0.0:
@@ -471,11 +500,15 @@ def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
painter.rotate(90)
painter.setPen(text_pen)
painter.drawText(
- QtCore.QRectF(2.5 + 1, -10, 40, 20), str(label_value), x_text_origin
+ QtCore.QRectF(2.5 + 1, -10, 40, 20),
+ str(label_value),
+ x_text_origin,
)
painter.restore()
- def drawForeground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
+ def drawForeground(
+ self, painter: QtGui.QPainter, rect: QtCore.QRectF
+ ) -> None:
"""Draw nearest sample point and coordinates."""
if not self._curve_init:
return
@@ -484,7 +517,9 @@ def drawForeground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
font.setPixelSize(self.FONT_HEIGHT)
painter.setFont(font)
- text_origin = QtGui.QTextOption(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ text_origin = QtGui.QTextOption(
+ QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
+ )
text_origin.setWrapMode(QtGui.QTextOption.NoWrap)
sample_l = sample_t = None
@@ -518,12 +553,18 @@ def drawForeground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
y_label_value = f"{nearest_sample[2]:.05f}"
painter.save()
- painter.translate(QtCore.QPointF(sample_l, sample_t + (20 * i)))
+ painter.translate(
+ QtCore.QPointF(sample_l, sample_t + (20 * i))
+ )
painter.scale(1, -1)
painter.setPen(GRAY_COLOR)
- painter.drawText(QtCore.QRectF(0, -20, 5, 10), "X:", text_origin)
- painter.drawText(QtCore.QRectF(0, -10, 5, 10), "Y:", text_origin)
+ painter.drawText(
+ QtCore.QRectF(0, -20, 5, 10), "X:", text_origin
+ )
+ painter.drawText(
+ QtCore.QRectF(0, -10, 5, 10), "Y:", text_origin
+ )
if color_name == GRAY_COLOR.name():
palette = self.palette()
@@ -541,7 +582,9 @@ def drawForeground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
def _invalidate(self) -> None:
"""Force repaint of visible region of graphics scene."""
- self._scene.invalidate(QtCore.QRectF(self.visibleRegion().boundingRect()))
+ self._scene.invalidate(
+ QtCore.QRectF(self.visibleRegion().boundingRect())
+ )
def _update_curves(self) -> None:
"""
@@ -549,8 +592,13 @@ def _update_curves(self) -> None:
processor.
"""
self._update_x_samples()
- if self._prev_cpu_proc is not None:
- self._on_processor_ready(self._prev_cpu_proc)
+ if (
+ self._prev_proc_context is not None
+ and self._prev_cpu_proc is not None
+ ):
+ self._on_processor_ready(
+ self._prev_proc_context, self._prev_cpu_proc
+ )
def _update_x_samples(self):
"""
@@ -558,13 +606,22 @@ def _update_x_samples(self):
parameters.
"""
self._x_lin = np.linspace(
- self._input_min, self._input_max, self._sample_count, dtype=np.float32
+ self._input_min,
+ self._input_max,
+ self._sample_count,
+ dtype=np.float32,
)
log_min = math.log(max(self.EPSILON, self._input_min))
- log_max = max(log_min + 0.00001, math.log(self._input_max, self._log_base))
+ log_max = max(
+ log_min + 0.00001, math.log(self._input_max, self._log_base)
+ )
self._x_log = np.logspace(
- log_min, log_max, self._sample_count, base=self._log_base, dtype=np.float32
+ log_min,
+ log_max,
+ self._sample_count,
+ base=self._log_base,
+ dtype=np.float32,
)
self._x_min = self._x_lin.min()
@@ -589,12 +646,16 @@ def _fit(self) -> None:
fm.boundingRect(
text_rect,
text_flags,
- "100.01" if self._sample_type == SampleType.LINEAR else "100.00001",
+ "100.01"
+ if self._sample_type == SampleType.LINEAR
+ else "100.00001",
).width()
+ 10
)
pad_l = fm.boundingRect(text_rect, text_flags, "100.01").width() + 10
- pad_r = fm.boundingRect(text_rect, text_flags, "X: 100.00001").width() + 10
+ pad_r = (
+ fm.boundingRect(text_rect, text_flags, "X: 100.00001").width() + 10
+ )
fit_rect = self._curve_rect.adjusted(-pad_l, -pad_t, pad_r, pad_b)
@@ -615,6 +676,7 @@ def _on_processor_ready(
"""
self.reset()
+ self._prev_proc_context = proc_context
self._prev_cpu_proc = cpu_proc
# Get input samples
@@ -635,13 +697,15 @@ def _on_processor_ready(
b_samples = rgb_samples[2::3]
# Collect sample pairs and min/max Y sample values
- if np.allclose(r_samples, g_samples, atol=self.EPSILON) and np.allclose(
- r_samples, b_samples, atol=self.EPSILON
- ):
+ if np.allclose(
+ r_samples, g_samples, atol=self.EPSILON
+ ) and np.allclose(r_samples, b_samples, atol=self.EPSILON):
palette = self.palette()
color_name = palette.color(palette.ColorRole.Text).name()
- self._samples[color_name] = np.stack((self._x_lin, r_samples), axis=-1)
+ self._samples[color_name] = np.stack(
+ (self._x_lin, r_samples), axis=-1
+ )
self._y_min = r_samples.min()
self._y_max = r_samples.max()
@@ -702,14 +766,18 @@ def _on_processor_ready(
for color_name, channel_samples in self._samples.items():
curve = QtGui.QPainterPath(
self._curve_tf.map(
- QtCore.QPointF(channel_samples[0][0], channel_samples[0][1])
+ QtCore.QPointF(
+ channel_samples[0][0], channel_samples[0][1]
+ )
)
)
curve.reserve(channel_samples.shape[0])
for i in range(1, channel_samples.shape[0]):
curve.lineTo(
self._curve_tf.map(
- QtCore.QPointF(channel_samples[i][0], channel_samples[i][1])
+ QtCore.QPointF(
+ channel_samples[i][0], channel_samples[i][1]
+ )
)
)
self._curve_paths[color_name] = curve
@@ -729,7 +797,9 @@ def _on_processor_ready(
# Expand scene rect to fit graph
max_dim = max(self._curve_rect.width(), self._curve_rect.height()) * 2
- scene_rect = self._curve_rect.adjusted(-max_dim, -max_dim, max_dim, max_dim)
+ scene_rect = self._curve_rect.adjusted(
+ -max_dim, -max_dim, max_dim, max_dim
+ )
self.setSceneRect(scene_rect)
self._fit()
diff --git a/src/apps/ocioview/ocioview/inspect/log_inspector.py b/src/apps/ocioview/ocioview/inspect/log_inspector.py
index b1d5a1c1ae..8675c24174 100644
--- a/src/apps/ocioview/ocioview/inspect/log_inspector.py
+++ b/src/apps/ocioview/ocioview/inspect/log_inspector.py
@@ -31,8 +31,12 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
# Widgets
self.log_level_box = ComboBox()
- self.log_level_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
- self.log_level_box.addItem("Warning", userData=ocio.LOGGING_LEVEL_WARNING)
+ self.log_level_box.setSizeAdjustPolicy(
+ QtWidgets.QComboBox.AdjustToContents
+ )
+ self.log_level_box.addItem(
+ "Warning", userData=ocio.LOGGING_LEVEL_WARNING
+ )
self.log_level_box.addItem("Info", userData=ocio.LOGGING_LEVEL_INFO)
self.log_level_box.addItem("Debug", userData=ocio.LOGGING_LEVEL_DEBUG)
self.log_level_box.setCurrentText(
@@ -55,7 +59,9 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.setLayout(layout)
# Initialize
- self.log_level_box.currentIndexChanged[int].connect(self._on_log_level_changed)
+ self.log_level_box.currentIndexChanged[int].connect(
+ self._on_log_level_changed
+ )
self.clear_button.released.connect(self.reset)
log_router = MessageRouter.get_instance()
diff --git a/src/apps/ocioview/ocioview/inspect_dock.py b/src/apps/ocioview/ocioview/inspect_dock.py
index c9ef01a6cb..99fcab7f2a 100644
--- a/src/apps/ocioview/ocioview/inspect_dock.py
+++ b/src/apps/ocioview/ocioview/inspect_dock.py
@@ -5,7 +5,12 @@
from PySide6 import QtCore, QtWidgets
-from .inspect import ChromaticitiesInspector, CodeInspector, CurveInspector, LogInspector
+from .inspect import (
+ ChromaticitiesInspector,
+ CodeInspector,
+ CurveInspector,
+ LogInspector,
+)
from .utils import get_glyph_icon
from .widgets.structure import TabbedDockWidget
@@ -16,8 +21,21 @@ class InspectDock(TabbedDockWidget):
transform data.
"""
- def __init__(self, parent: Optional[QtCore.QObject] = None):
- super().__init__("Inspect", get_glyph_icon("mdi6.dna"), parent=parent)
+ def __init__(
+ self,
+ corner_widget: Optional[QtWidgets.QWidget] = None,
+ parent: Optional[QtCore.QObject] = None,
+ ):
+ """
+ :param corner_widget: Optional widget to place on the right
+ side of the dock title bar.
+ """
+ super().__init__(
+ "Inspect",
+ get_glyph_icon("mdi6.dna"),
+ corner_widget=corner_widget,
+ parent=parent,
+ )
self.setAllowedAreas(
QtCore.Qt.BottomDockWidgetArea | QtCore.Qt.TopDockWidgetArea
@@ -53,8 +71,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
)
def reset(self) -> None:
- """Reset data for all inspectors."""
+ """Reset data for all inspectors, except the log."""
self.chromaticities_inspector.reset()
self.curve_inspector.reset()
self.code_inspector.reset()
- self.log_inspector.reset()
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 76d0ffbefb..a13ea05f39 100644
--- a/src/apps/ocioview/ocioview/items/active_display_view_edit.py
+++ b/src/apps/ocioview/ocioview/items/active_display_view_edit.py
@@ -61,7 +61,9 @@ class ActiveDisplayViewEdit(QtWidgets.QWidget):
@classmethod
def item_type_icon(cls) -> QtGui.QIcon:
- return get_glyph_icon("mdi6.sort-bool-ascending-variant", size=ICON_SIZE_ITEM)
+ 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/active_display_view_model.py b/src/apps/ocioview/ocioview/items/active_display_view_model.py
index 63f1d5fd37..47c4db06d0 100644
--- a/src/apps/ocioview/ocioview/items/active_display_view_model.py
+++ b/src/apps/ocioview/ocioview/items/active_display_view_model.py
@@ -64,7 +64,9 @@ def move_item_up(self, item_name: str) -> bool:
if dst_row == src_row:
return False
- return self.moveRows(self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row)
+ return self.moveRows(
+ self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row
+ )
def move_item_down(self, item_name: str) -> bool:
active_names = self.__get_active_items__()
@@ -77,7 +79,9 @@ def move_item_down(self, item_name: str) -> bool:
if dst_row == src_row:
return False
- return self.moveRows(self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row)
+ return self.moveRows(
+ self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row
+ )
def flags(self, index: QtCore.QModelIndex) -> int:
return super().flags(index) | QtCore.Qt.ItemIsUserCheckable
diff --git a/src/apps/ocioview/ocioview/items/color_space_edit.py b/src/apps/ocioview/ocioview/items/color_space_edit.py
index b93b2b9093..8b451aa146 100644
--- a/src/apps/ocioview/ocioview/items/color_space_edit.py
+++ b/src/apps/ocioview/ocioview/items/color_space_edit.py
@@ -9,6 +9,7 @@
from ..config_cache import ConfigCache
from ..constants import ICON_SIZE_ITEM
+from ..signal_router import SignalRouter
from ..utils import get_glyph_icon
from ..widgets import (
CheckBox,
@@ -50,10 +51,16 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
)
self.aliases_list = StringListWidget(
item_basename="alias",
- item_icon=get_glyph_icon("ph.bookmark-simple", size=ICON_SIZE_ITEM),
+ 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.family_edit = CallbackComboBox(ConfigCache.get_families, editable=True)
- self.encoding_edit = CallbackComboBox(ConfigCache.get_encodings, editable=True)
self.equality_group_edit = CallbackComboBox(
ConfigCache.get_equality_groups, editable=True
)
@@ -61,34 +68,49 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
self.bit_depth_combo = EnumComboBox(ocio.BitDepth)
self.is_data_check = CheckBox()
self.allocation_combo = EnumComboBox(ocio.Allocation)
- self.allocation_combo.currentIndexChanged.connect(self._on_allocation_changed)
+ self.allocation_combo.currentIndexChanged.connect(
+ self._on_allocation_changed
+ )
self.allocation_vars_edit = FloatEditArray(
("min", "max", "offset"), (0.0, 1.0, 0.0)
)
self.categories_list = StringListWidget(
item_basename="category",
- item_icon=get_glyph_icon("ph.bookmarks-simple", size=ICON_SIZE_ITEM),
+ item_icon=get_glyph_icon(
+ "ph.bookmarks-simple", size=ICON_SIZE_ITEM
+ ),
get_presets=self._get_available_categories,
)
# Layout
self._param_layout.addRow(
- self.model.REFERENCE_SPACE_TYPE.label, self.reference_space_type_combo
+ self.model.REFERENCE_SPACE_TYPE.label,
+ self.reference_space_type_combo,
)
self._param_layout.addRow(self.model.ALIASES.label, self.aliases_list)
self._param_layout.addRow(self.model.FAMILY.label, self.family_edit)
- self._param_layout.addRow(self.model.ENCODING.label, self.encoding_edit)
+ self._param_layout.addRow(
+ self.model.ENCODING.label, self.encoding_edit
+ )
self._param_layout.addRow(
self.model.EQUALITY_GROUP.label, self.equality_group_edit
)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
- self._param_layout.addRow(self.model.BIT_DEPTH.label, self.bit_depth_combo)
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
+ self._param_layout.addRow(
+ self.model.BIT_DEPTH.label, self.bit_depth_combo
+ )
self._param_layout.addRow(self.model.IS_DATA.label, self.is_data_check)
- self._param_layout.addRow(self.model.ALLOCATION.label, self.allocation_combo)
+ self._param_layout.addRow(
+ self.model.ALLOCATION.label, self.allocation_combo
+ )
self._param_layout.addRow(
self.model.ALLOCATION_VARS.label, self.allocation_vars_edit
)
- self._param_layout.addRow(self.model.CATEGORIES.label, self.categories_list)
+ self._param_layout.addRow(
+ self.model.CATEGORIES.label, self.categories_list
+ )
def update_available_allocation_vars(self) -> None:
"""
@@ -115,7 +137,11 @@ def _get_available_categories(self) -> list[str]:
to this item.
"""
current_categories = self.categories_list.items()
- return [c for c in ConfigCache.get_categories() if c not in current_categories]
+ return [
+ c
+ for c in ConfigCache.get_categories()
+ if c not in current_categories
+ ]
class ColorSpaceEdit(BaseConfigItemEdit):
@@ -124,6 +150,7 @@ class ColorSpaceEdit(BaseConfigItemEdit):
"""
__param_edit_type__ = ColorSpaceParamEdit
+ __signal_router_emit__ = SignalRouter.emit_color_spaces_changed.__name__
def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent=parent)
@@ -131,41 +158,53 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.reference_space_type_combo,
model.REFERENCE_SPACE_TYPE.column,
)
- self._mapper.addMapping(self.param_edit.aliases_list, model.ALIASES.column)
- self._mapper.addMapping(self.param_edit.family_edit, model.FAMILY.column)
- self._mapper.addMapping(self.param_edit.encoding_edit, model.ENCODING.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(
+ self.param_edit.aliases_list, model.ALIASES.column
+ )
+ self.mapper.addMapping(
+ self.param_edit.family_edit, model.FAMILY.column
+ )
+ self.mapper.addMapping(
+ self.param_edit.encoding_edit, model.ENCODING.column
+ )
+ self.mapper.addMapping(
self.param_edit.equality_group_edit, model.EQUALITY_GROUP.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
- self._mapper.addMapping(self.param_edit.bit_depth_combo, model.BIT_DEPTH.column)
- self._mapper.addMapping(self.param_edit.is_data_check, model.IS_DATA.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(
+ self.param_edit.bit_depth_combo, model.BIT_DEPTH.column
+ )
+ self.mapper.addMapping(
+ self.param_edit.is_data_check, model.IS_DATA.column
+ )
+ self.mapper.addMapping(
self.param_edit.allocation_combo, model.ALLOCATION.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.allocation_vars_edit, model.ALLOCATION_VARS.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.categories_list, model.CATEGORIES.column
)
# list widgets need manual data submission back to model
- self.param_edit.aliases_list.items_changed.connect(self._mapper.submit)
- self.param_edit.categories_list.items_changed.connect(self._mapper.submit)
+ self.param_edit.aliases_list.items_changed.connect(self.mapper.submit)
+ self.param_edit.categories_list.items_changed.connect(
+ self.mapper.submit
+ )
# Trigger immediate update from widgets that update the model upon losing focus
self.param_edit.reference_space_type_combo.currentIndexChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
self.param_edit.is_data_check.stateChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
# Initialize
diff --git a/src/apps/ocioview/ocioview/items/color_space_model.py b/src/apps/ocioview/ocioview/items/color_space_model.py
index 4e71469888..bc65316499 100644
--- a/src/apps/ocioview/ocioview/items/color_space_model.py
+++ b/src/apps/ocioview/ocioview/items/color_space_model.py
@@ -51,7 +51,9 @@ 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", size=ICON_SIZE_ITEM),
+ 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
),
@@ -70,7 +72,9 @@ def get_item_transforms(
# 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()
+ ref_space_name = (
+ ReferenceSpaceManager.scene_reference_space().getName()
+ )
return (
ocio.ColorSpaceTransform(src=ref_space_name, dst=item_name),
ocio.ColorSpaceTransform(src=item_name, dst=ref_space_name),
@@ -124,10 +128,14 @@ def _remove_item(self, item: ocio.ColorSpace) -> None:
def _new_item(self, name: str) -> None:
ocio.GetCurrentConfig().addColorSpace(
- ocio.ColorSpace(referenceSpace=ocio.REFERENCE_SPACE_SCENE, name=name)
+ ocio.ColorSpace(
+ referenceSpace=ocio.REFERENCE_SPACE_SCENE, name=name
+ )
)
- def _get_value(self, item: ocio.ColorSpace, column_desc: ColumnDesc) -> Any:
+ def _get_value(
+ self, item: ocio.ColorSpace, column_desc: ColumnDesc
+ ) -> Any:
# Get parameters
if column_desc == self.REFERENCE_SPACE_TYPE:
return int(item.getReferenceSpaceType().value)
@@ -160,7 +168,9 @@ def _get_value(self, item: ocio.ColorSpace, column_desc: ColumnDesc) -> Any:
num_alloc_vars = len(alloc_vars)
if num_alloc_vars < 3:
default_alloc_vars = [0.0, 1.0, 0.0]
- alloc_vars += [default_alloc_vars[i] for i in range(num_alloc_vars, 3)]
+ alloc_vars += [
+ default_alloc_vars[i] for i in range(num_alloc_vars, 3)
+ ]
elif num_alloc_vars > 3:
alloc_vars = alloc_vars[:3]
return alloc_vars
@@ -203,8 +213,12 @@ def _set_value(
isData=item.isData(),
allocation=item.getAllocation(),
allocationVars=item.getAllocationVars(),
- toReference=item.getTransform(ocio.COLORSPACE_DIR_TO_REFERENCE),
- fromReference=item.getTransform(ocio.COLORSPACE_DIR_FROM_REFERENCE),
+ toReference=item.getTransform(
+ ocio.COLORSPACE_DIR_TO_REFERENCE
+ ),
+ fromReference=item.getTransform(
+ ocio.COLORSPACE_DIR_FROM_REFERENCE
+ ),
categories=list(item.getCategories()),
)
@@ -214,7 +228,8 @@ def _set_value(
# Update parameters
if column_desc == self.NAME:
- new_item.setName(value)
+ if value:
+ new_item.setName(value)
elif column_desc == self.ALIASES:
new_item.clearAliases()
for alias in value:
@@ -275,5 +290,6 @@ def _set_value(
if column_desc in (self.NAME, self.TO_REFERENCE, self.FROM_REFERENCE):
item_name = new_item.getName()
self._update_tf_subscribers(
- item_name, prev_item_name if prev_item_name != item_name else None
+ item_name,
+ prev_item_name if prev_item_name != item_name else None,
)
diff --git a/src/apps/ocioview/ocioview/items/config_item_edit.py b/src/apps/ocioview/ocioview/items/config_item_edit.py
index e96a78de06..658e1fc45a 100644
--- a/src/apps/ocioview/ocioview/items/config_item_edit.py
+++ b/src/apps/ocioview/ocioview/items/config_item_edit.py
@@ -7,6 +7,7 @@
from PySide6 import QtCore, QtGui, QtWidgets
from ..constants import MARGIN_WIDTH, ICON_SIZE_TAB
+from ..signal_router import SignalRouter
from ..transform_manager import TransformManager
from ..transforms import TransformEditStack
from ..utils import get_glyph_icon, SignalsBlocked
@@ -50,11 +51,15 @@ 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", size=ICON_SIZE_TAB)
+ 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, size=ICON_SIZE_TAB
)
- self._to_ref_icon = get_glyph_icon("mdi6.layers-minus", size=ICON_SIZE_TAB)
+ 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, size=ICON_SIZE_TAB
)
@@ -84,7 +89,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
param_frame.setLayout(param_spacer_layout)
param_scroll_area = QtWidgets.QScrollArea()
- param_scroll_area.setObjectName("config_item_param_edit__param_scroll_area")
+ param_scroll_area.setObjectName(
+ "config_item_param_edit__param_scroll_area"
+ )
param_scroll_area.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
@@ -200,6 +207,9 @@ class BaseConfigItemEdit(QtWidgets.QWidget):
# By default, this inherits from __has_list__.
__has_splitter__: bool = None
+ # If set, call the named signal router method on model change
+ __signal_router_emit__: str = None
+
@classmethod
def item_type_icon(cls) -> QtGui.QIcon:
"""
@@ -213,7 +223,9 @@ def item_type_label(cls, plural: bool = False) -> str:
:param plural: Whether label should be plural
:return: Friendly type name
"""
- return cls.__param_edit_type__.__model_type__.item_type_label(plural=plural)
+ return cls.__param_edit_type__.__model_type__.item_type_label(
+ plural=plural
+ )
def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent=parent)
@@ -225,6 +237,16 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
+ # Connect signal router to model change
+ if self.__signal_router_emit__:
+ signal_router = SignalRouter.get_instance()
+ emit_method = getattr(signal_router, self.__signal_router_emit__)
+ model.dataChanged.connect(lambda *a, **kw: emit_method())
+ model.item_renamed.connect(lambda *a, **kw: emit_method())
+ model.item_added.connect(lambda *a, **kw: emit_method())
+ model.item_moved.connect(lambda *a, **kw: emit_method())
+ model.item_removed.connect(lambda *a, **kw: emit_method())
+
if self.__has_list__:
self.list = ItemModelListWidget(
model, model.NAME.column, item_icon=self.item_type_icon()
@@ -235,16 +257,20 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model.item_selection_requested.connect(
lambda index: self.list.set_current_row(index.row())
)
- model.modelAboutToBeReset.connect(lambda: self.param_edit.setEnabled(False))
+ model.modelAboutToBeReset.connect(
+ lambda: self.param_edit.setEnabled(False)
+ )
# Map widgets to model columns
- self._mapper = QtWidgets.QDataWidgetMapper()
- self._mapper.setOrientation(QtCore.Qt.Horizontal)
- self._mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.AutoSubmit)
- self._mapper.setModel(model)
+ self.mapper = QtWidgets.QDataWidgetMapper()
+ self.mapper.setOrientation(QtCore.Qt.Horizontal)
+ self.mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.AutoSubmit)
+ self.mapper.setModel(model)
try:
- self._mapper.addMapping(self.param_edit.name_edit, model.NAME.column)
+ self.mapper.addMapping(
+ self.param_edit.name_edit, model.NAME.column
+ )
except RuntimeError:
# Some derived classes may delete this widget to handle custom mapping
pass
@@ -254,7 +280,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
# in transit. Conversely, handling transform updates manually via
# signals/slots is stable, so used here instead.
if self.param_edit.__has_transforms__:
- self.param_edit.from_ref_stack.edited.connect(self._on_from_ref_edited)
+ self.param_edit.from_ref_stack.edited.connect(
+ self._on_from_ref_edited
+ )
self.param_edit.to_ref_stack.edited.connect(self._on_to_ref_edited)
model.dataChanged.connect(self._on_data_changed)
@@ -292,7 +320,9 @@ def set_splitter_sizes(self, from_sizes: list[int]) -> None:
adapt_splitter_sizes(from_sizes, self.splitter.sizes())
)
- def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
+ def eventFilter(
+ self, watched: QtCore.QObject, event: QtCore.QEvent
+ ) -> bool:
"""
Handle setting subscription for the current item's transform on
number key press.
@@ -316,7 +346,9 @@ def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
)
):
current_index = self.list.current_index()
- item_label = self.model.format_subscription_item_label(current_index)
+ item_label = self.model.format_subscription_item_label(
+ current_index
+ )
if item_label:
TransformManager.set_subscription(
int(event.text()), self.model, item_label
@@ -334,19 +366,21 @@ def _on_current_row_changed(self, row: int) -> None:
if row < 0:
self.param_edit.reset()
else:
- self._mapper.setCurrentIndex(row)
+ self.mapper.setCurrentIndex(row)
# Manually update transform stacks from model, on current row change
if self.param_edit.__has_transforms__:
model = self.model
with SignalsBlocked(
- self.param_edit.from_ref_stack, self.param_edit.to_ref_stack
+ self.param_edit.from_ref_stack,
+ self.param_edit.to_ref_stack,
):
self.param_edit.from_ref_stack.set_transform(
model.data(
model.index(
- row, self.param_edit.__from_ref_column_desc__.column
+ row,
+ self.param_edit.__from_ref_column_desc__.column,
),
QtCore.Qt.EditRole,
)
@@ -354,7 +388,8 @@ def _on_current_row_changed(self, row: int) -> None:
self.param_edit.to_ref_stack.set_transform(
model.data(
model.index(
- row, self.param_edit.__to_ref_column_desc__.column
+ row,
+ self.param_edit.__to_ref_column_desc__.column,
),
QtCore.Qt.EditRole,
)
@@ -363,7 +398,10 @@ def _on_current_row_changed(self, row: int) -> None:
@QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex, list)
def _on_data_changed(
- self, top_left: QtCore.QModelIndex, bottom_right: QtCore.QModelIndex, roles=()
+ self,
+ top_left: QtCore.QModelIndex,
+ bottom_right: QtCore.QModelIndex,
+ roles=(),
) -> None:
"""
Manually update transform stacks from model, on model data
diff --git a/src/apps/ocioview/ocioview/items/config_item_model.py b/src/apps/ocioview/ocioview/items/config_item_model.py
index e77b4a7fcd..19505c1058 100644
--- a/src/apps/ocioview/ocioview/items/config_item_model.py
+++ b/src/apps/ocioview/ocioview/items/config_item_model.py
@@ -75,7 +75,9 @@ def item_type_icon(cls) -> QtGui.QIcon:
:return: Item type icon
"""
if cls.__icon__ is None:
- cls.__icon__ = get_glyph_icon(cls.__icon_glyph__, size=ICON_SIZE_ITEM)
+ cls.__icon__ = get_glyph_icon(
+ cls.__icon_glyph__, size=ICON_SIZE_ITEM
+ )
return cls.__icon__
@classmethod
@@ -221,7 +223,9 @@ def move_item_up(self, item_name: str) -> bool:
if dst_row == src_row:
return False
- return self.moveRows(self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row)
+ return self.moveRows(
+ self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row
+ )
def move_item_down(self, item_name: str) -> bool:
"""
@@ -240,7 +244,9 @@ def move_item_down(self, item_name: str) -> bool:
if dst_row == src_row:
return False
- return self.moveRows(self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row)
+ return self.moveRows(
+ self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row
+ )
def flags(self, index: QtCore.QModelIndex) -> int:
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
@@ -263,10 +269,15 @@ def headerData(
"""
:return: Column labels
"""
- if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
+ if (
+ orientation == QtCore.Qt.Horizontal
+ and role == QtCore.Qt.DisplayRole
+ ):
return self.COLUMNS[column].label
- def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole) -> Any:
+ def data(
+ self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole
+ ) -> Any:
"""
:return: Item data pulled from the current config for the
index-referenced column.
@@ -293,7 +304,10 @@ def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole) ->
return None
def setData(
- self, index: QtCore.QModelIndex, value: Any, role: int = QtCore.Qt.EditRole
+ self,
+ index: QtCore.QModelIndex,
+ value: Any,
+ role: int = QtCore.Qt.EditRole,
) -> bool:
"""
Push modified item data to the current config for the
@@ -312,7 +326,7 @@ def setData(
if checked_column_desc is not None:
column_desc = checked_column_desc
index = index.sibling(index.row(), column_desc.column)
- value = value == QtCore.Qt.Checked
+ value = value == QtCore.Qt.Checked.value
role = QtCore.Qt.EditRole
undo_cmd_type = self._get_undo_command_type(column_desc)
@@ -419,7 +433,9 @@ def moveRows(
all_names = self.get_item_names()
all_items = {
name: item
- for name, item in zip(all_names, self._get_items(preserve=True))
+ for name, item in zip(
+ all_names, self._get_items(preserve=True)
+ )
}
insert_before = None
@@ -427,7 +443,8 @@ def moveRows(
insert_before = all_names[dst_row]
move_names = [
- all_names.pop(i) for i in reversed(range(src_row, src_row + count))
+ all_names.pop(i)
+ for i in reversed(range(src_row, src_row + count))
]
if insert_before is not None:
@@ -436,7 +453,9 @@ def moveRows(
new_dst_row = len(all_names)
with ConfigSnapshotUndoCommand(
- f"Move {self.item_type_label()}", model=self, item_name=move_names[0]
+ f"Move {self.item_type_label()}",
+ model=self,
+ item_name=move_names[0],
):
for i in range(len(move_names)):
all_names.insert(new_dst_row, move_names[i])
@@ -459,40 +478,45 @@ def removeRows(
Remove ``count`` items from the current config, starting at
``row`` index.
"""
- self.beginRemoveRows(parent, row, row + count - 1)
-
items = self._get_items()
item_names = self.get_item_names()
num_items = len(items)
- do_not_remove = []
+ remove_rows = []
+ could_not_remove = []
+
+ for i in range(row, row + count):
+ if i < num_items:
+ item = items[i]
+ can_be_removed, reason = self._can_item_be_removed(item)
+ if not can_be_removed:
+ could_not_remove.append((item_names[i], reason))
+ else:
+ remove_rows.append(i)
+
+ if remove_rows:
+ with ConfigSnapshotUndoCommand(
+ f"Delete {self.item_type_label()}",
+ model=self,
+ item_name=item_names[row],
+ ):
+ self.beginRemoveRows(parent, row, row + count - 1)
- with ConfigSnapshotUndoCommand(
- f"Delete {self.item_type_label()}", model=self, item_name=item_names[row]
- ):
- for i in reversed(range(row, row + count)):
- if i < num_items:
- item = items[i]
- can_be_removed, reason = self._can_item_be_removed(item)
- if not can_be_removed:
- do_not_remove.append((item_names[i], reason))
- else:
- self._remove_item(item)
-
- if num_items:
- self.item_removed.emit()
+ for i in reversed(remove_rows):
+ self._remove_item(items[i])
- self.endRemoveRows()
+ self.item_removed.emit()
+ self.endRemoveRows()
# Warn user about refused item removals
- if do_not_remove:
+ if could_not_remove:
item_warning_lines = []
- for item_name, reason in do_not_remove:
+ for item_name, reason in could_not_remove:
item_warning_lines.append(f"{item_name} {reason}")
item_warnings = "
".join(item_warning_lines)
self.warning_raised.emit(
- f"{len(do_not_remove)} "
- f"{self.item_type_label(plural=len(do_not_remove) != 1).lower()} could "
+ f"{len(could_not_remove)} "
+ f"{self.item_type_label(plural=len(could_not_remove) != 1).lower()} could "
f"not be removed:
{item_warnings}"
)
@@ -552,7 +576,9 @@ def get_item_transforms(
"""
return None, None
- def get_index_from_item_name(self, item_name: str) -> Optional[QtCore.QModelIndex]:
+ def get_index_from_item_name(
+ self, item_name: str
+ ) -> Optional[QtCore.QModelIndex]:
"""
Lookup the model index for the named item.
@@ -631,7 +657,9 @@ def _can_item_be_removed(self, item: __item_type__) -> tuple[bool, str]:
"""
return True, ""
- def _get_display(self, item: __item_type__, column_desc: ColumnDesc) -> str:
+ def _get_display(
+ self, item: __item_type__, column_desc: ColumnDesc
+ ) -> str:
"""
:return: Display role value for a given model column
"""
@@ -663,7 +691,9 @@ def _get_subscription_color(
"""
slot = TransformManager.get_subscription_slot(
self,
- self.format_subscription_item_label(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
@@ -678,7 +708,9 @@ def _get_subscription_icon(
"""
slot = TransformManager.get_subscription_slot(
self,
- self.format_subscription_item_label(self._get_value(item, column_desc)),
+ self.format_subscription_item_label(
+ self._get_value(item, column_desc)
+ ),
)
return TransformManager.get_subscription_slot_icon(slot)
@@ -754,7 +786,9 @@ def _update_tf_subscribers(
# the model.
item_label = self.format_subscription_item_label(item_name)
if prev_item_name:
- prev_item_label = self.format_subscription_item_label(prev_item_name)
+ prev_item_label = self.format_subscription_item_label(
+ prev_item_name
+ )
else:
prev_item_label = None
diff --git a/src/apps/ocioview/ocioview/items/config_properties_edit.py b/src/apps/ocioview/ocioview/items/config_properties_edit.py
index ea2bca0354..9eb42b54ea 100644
--- a/src/apps/ocioview/ocioview/items/config_properties_edit.py
+++ b/src/apps/ocioview/ocioview/items/config_properties_edit.py
@@ -54,12 +54,18 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
# Layout
self._param_layout.addRow(self.model.VERSION.label, self.version_edit)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
self._param_layout.addRow(
self.model.ENVIRONMENT_VARS.label, self.env_vars_table
)
- self._param_layout.addRow(self.model.SEARCH_PATH.label, self.search_path_list)
- self._param_layout.addRow(self.model.WORKING_DIR.label, self.working_dir_edit)
+ self._param_layout.addRow(
+ self.model.SEARCH_PATH.label, self.search_path_list
+ )
+ self._param_layout.addRow(
+ self.model.WORKING_DIR.label, self.working_dir_edit
+ )
self._param_layout.addRow(
self.model.FAMILY_SEPARATOR.label, self.family_separator_edit
)
@@ -106,33 +112,40 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(self.param_edit.version_edit, model.VERSION.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(
+ self.param_edit.version_edit, model.VERSION.column
+ )
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.env_vars_table, model.ENVIRONMENT_VARS.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.search_path_list, model.SEARCH_PATH.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.working_dir_edit, model.WORKING_DIR.column
)
- self._mapper.addMapping(
- self.param_edit.family_separator_edit, model.FAMILY_SEPARATOR.column
+ self.mapper.addMapping(
+ self.param_edit.family_separator_edit,
+ model.FAMILY_SEPARATOR.column,
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.default_luma_coefs_edit,
model.DEFAULT_LUMA_COEFS.column,
)
# Table and list widgets need manual data submission back to model
- self.param_edit.env_vars_table.items_changed.connect(self._mapper.submit)
- self.param_edit.search_path_list.items_changed.connect(self._mapper.submit)
+ self.param_edit.env_vars_table.items_changed.connect(
+ self.mapper.submit
+ )
+ self.param_edit.search_path_list.items_changed.connect(
+ self.mapper.submit
+ )
# Reload sole item on reset
- model.modelReset.connect(lambda: self._mapper.setCurrentIndex(0))
+ model.modelReset.connect(lambda: self.mapper.setCurrentIndex(0))
# Initialize
- self._mapper.setCurrentIndex(0)
+ self.mapper.setCurrentIndex(0)
diff --git a/src/apps/ocioview/ocioview/items/display_model.py b/src/apps/ocioview/ocioview/items/display_model.py
index 1f8c782133..78d80a095f 100644
--- a/src/apps/ocioview/ocioview/items/display_model.py
+++ b/src/apps/ocioview/ocioview/items/display_model.py
@@ -20,7 +20,7 @@ class View:
type: ViewType
name: str
color_space: str
- view_transform: str
+ view_transform: str = ""
looks: str = ""
rule: str = ""
description: str = ""
@@ -68,38 +68,46 @@ def _get_items(self, preserve: bool = False) -> list[Display]:
self._items = []
# Get views to preserve through display name changes
- for name in ConfigCache.get_displays():
+ for name in config.getDisplays():
display = Display(name)
# Display-defined views
- for view in ConfigCache.get_views(
- name, view_type=ocio.VIEW_DISPLAY_DEFINED
- ):
- display.views.append(
- View(
- get_view_type(name, view),
- view,
+ for view in config.getViews(ocio.VIEW_DISPLAY_DEFINED, name):
+ view_type = get_view_type(name, view)
+
+ if view_type == ViewType.VIEW_SCENE:
+ view_ref = View(
+ view_type,
+ name,
+ config.getDisplayViewColorSpaceName(name, view),
+ looks=config.getDisplayViewLooks(name, view),
+ )
+ display.views.append(view_ref)
+
+ else: # VIEW_DISPLAY
+ view_ref = View(
+ view_type,
+ name,
config.getDisplayViewColorSpaceName(name, view),
config.getDisplayViewTransformName(name, view),
config.getDisplayViewLooks(name, view),
config.getDisplayViewRule(name, view),
config.getDisplayViewDescription(name, view),
)
- )
+ display.views.append(view_ref)
# Shared views
- for view in ConfigCache.get_views(name, view_type=ocio.VIEW_SHARED):
- display.views.append(
- View(
- ViewType.VIEW_SHARED,
- view,
- config.getDisplayViewColorSpaceName("", view),
- config.getDisplayViewTransformName("", view),
- config.getDisplayViewLooks("", view),
- config.getDisplayViewRule("", view),
- config.getDisplayViewDescription("", view),
- )
+ for view in config.getViews(ocio.VIEW_SHARED, name):
+ view_ref = View(
+ ViewType.VIEW_SHARED,
+ view,
+ config.getDisplayViewColorSpaceName("", view),
+ config.getDisplayViewTransformName("", view),
+ config.getDisplayViewLooks("", view),
+ config.getDisplayViewRule("", view),
+ config.getDisplayViewDescription("", view),
)
+ display.views.append(view_ref)
self._items.append(display)
@@ -175,7 +183,9 @@ def _new_item(self, name: str) -> None:
displayColorSpaceName=color_space,
)
else:
- config.addDisplayView(name, new_view, colorSpaceName=color_space)
+ config.addDisplayView(
+ name, new_view, colorSpaceName=color_space
+ )
def _get_value(self, item: Display, column_desc: ColumnDesc) -> Any:
# Get parameters
diff --git a/src/apps/ocioview/ocioview/items/file_rule_edit.py b/src/apps/ocioview/ocioview/items/file_rule_edit.py
index 95bda2c4b1..d3602a6b81 100644
--- a/src/apps/ocioview/ocioview/items/file_rule_edit.py
+++ b/src/apps/ocioview/ocioview/items/file_rule_edit.py
@@ -3,13 +3,13 @@
from typing import Optional
+import PyOpenColorIO as ocio
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,
+ ColorSpaceComboBox,
ExpandingStackedWidget,
FormLayout,
LineEdit,
@@ -49,9 +49,11 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
params_layout.addRow(self.model.NAME.label, name_edit)
if file_rule_type != FileRuleType.RULE_OCIO_V1:
- color_space_combo = CallbackComboBox(ConfigCache.get_color_space_names)
+ color_space_combo = ColorSpaceComboBox()
self.color_space_combos[file_rule_type] = color_space_combo
- params_layout.addRow(self.model.COLOR_SPACE.label, color_space_combo)
+ params_layout.addRow(
+ self.model.COLOR_SPACE.label, color_space_combo
+ )
if file_rule_type == FileRuleType.RULE_BASIC:
pattern_edit = LineEdit()
@@ -60,14 +62,19 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
extension_edit = LineEdit()
self.extension_edits[file_rule_type] = extension_edit
- params_layout.addRow(self.model.EXTENSION.label, extension_edit)
+ params_layout.addRow(
+ self.model.EXTENSION.label, extension_edit
+ )
if file_rule_type == FileRuleType.RULE_REGEX:
regex_edit = LineEdit()
self.regex_edits[file_rule_type] = regex_edit
params_layout.addRow(self.model.REGEX.label, regex_edit)
- if file_rule_type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX):
+ 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", size=ICON_SIZE_ITEM),
@@ -75,7 +82,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
default_value="value",
)
self.custom_keys_tables[file_rule_type] = custom_keys_table
- params_layout.addRow(self.model.CUSTOM_KEYS.label, custom_keys_table)
+ params_layout.addRow(
+ self.model.CUSTOM_KEYS.label, custom_keys_table
+ )
params = QtWidgets.QFrame()
params.setLayout(params_layout)
@@ -102,14 +111,18 @@ def update_available_params(
)
if file_rule_type in self.name_edits:
- mapper.addMapping(self.name_edits[file_rule_type], self.model.NAME.column)
+ mapper.addMapping(
+ self.name_edits[file_rule_type], self.model.NAME.column
+ )
self.name_edits[file_rule_type].setEnabled(
- file_rule_type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX)
+ file_rule_type
+ in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX)
)
if file_rule_type in self.color_space_combos:
mapper.addMapping(
- self.color_space_combos[file_rule_type], self.model.COLOR_SPACE.column
+ self.color_space_combos[file_rule_type],
+ self.model.COLOR_SPACE.column,
)
self.color_space_combos[file_rule_type].setEnabled(
file_rule_type != FileRuleType.RULE_OCIO_V1
@@ -124,14 +137,17 @@ def update_available_params(
)
if file_rule_type in self.regex_edits:
- mapper.addMapping(self.regex_edits[file_rule_type], self.model.REGEX.column)
+ mapper.addMapping(
+ self.regex_edits[file_rule_type], self.model.REGEX.column
+ )
self.regex_edits[file_rule_type].setEnabled(
file_rule_type == FileRuleType.RULE_REGEX
)
if file_rule_type in self.extension_edits:
mapper.addMapping(
- self.extension_edits[file_rule_type], self.model.EXTENSION.column
+ self.extension_edits[file_rule_type],
+ self.model.EXTENSION.column,
)
self.extension_edits[file_rule_type].setEnabled(
file_rule_type == FileRuleType.RULE_BASIC
@@ -139,10 +155,12 @@ def update_available_params(
if file_rule_type in self.custom_keys_tables:
mapper.addMapping(
- self.custom_keys_tables[file_rule_type], self.model.CUSTOM_KEYS.column
+ self.custom_keys_tables[file_rule_type],
+ self.model.CUSTOM_KEYS.column,
)
self.custom_keys_tables[file_rule_type].setEnabled(
- file_rule_type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX)
+ file_rule_type
+ in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX)
)
@@ -159,11 +177,11 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Clear default mapped widgets. Widgets will be remapped per file rule type.
- self._mapper.clearMapping()
+ self.mapper.clearMapping()
# Table widgets need manual data submission back to model
for custom_keys_table in self.param_edit.custom_keys_tables.values():
- custom_keys_table.items_changed.connect(self._mapper.submit)
+ custom_keys_table.items_changed.connect(self.mapper.submit)
# Initialize
if model.rowCount():
@@ -178,6 +196,8 @@ def _on_current_row_changed(self, row: int) -> None:
self.model.index(row, self.model.FILE_RULE_TYPE.column),
QtCore.Qt.EditRole,
)
- self.param_edit.update_available_params(self._mapper, file_rule_type)
+ self.param_edit.update_available_params(
+ self.mapper, file_rule_type
+ )
super()._on_current_row_changed(row)
diff --git a/src/apps/ocioview/ocioview/items/file_rule_model.py b/src/apps/ocioview/ocioview/items/file_rule_model.py
index 2cad0c54ec..170b1f69db 100644
--- a/src/apps/ocioview/ocioview/items/file_rule_model.py
+++ b/src/apps/ocioview/ocioview/items/file_rule_model.py
@@ -63,7 +63,15 @@ class FileRuleModel(BaseConfigItemModel):
CUSTOM_KEYS = ColumnDesc(6, "Custom Keys", list)
COLUMNS = sorted(
- [FILE_RULE_TYPE, NAME, COLOR_SPACE, PATTERN, REGEX, EXTENSION, CUSTOM_KEYS],
+ [
+ FILE_RULE_TYPE,
+ NAME,
+ COLOR_SPACE,
+ PATTERN,
+ REGEX,
+ EXTENSION,
+ CUSTOM_KEYS,
+ ],
key=lambda s: s.column,
)
@@ -106,8 +114,12 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__(parent=parent)
self._rule_type_icons = {
- FileRuleType.RULE_BASIC: self.get_rule_type_icon(FileRuleType.RULE_BASIC),
- FileRuleType.RULE_REGEX: self.get_rule_type_icon(FileRuleType.RULE_REGEX),
+ FileRuleType.RULE_BASIC: self.get_rule_type_icon(
+ FileRuleType.RULE_BASIC
+ ),
+ FileRuleType.RULE_REGEX: self.get_rule_type_icon(
+ FileRuleType.RULE_REGEX
+ ),
FileRuleType.RULE_OCIO_V1: self.get_rule_type_icon(
FileRuleType.RULE_OCIO_V1
),
@@ -146,7 +158,9 @@ def add_preset(self, preset_name: str) -> int:
ConfigCache.get_default_color_space_name(),
)
else:
- return file_rules.getIndexForRule(ocio.FILE_PATH_SEARCH_RULE_NAME)
+ return file_rules.getIndexForRule(
+ ocio.FILE_PATH_SEARCH_RULE_NAME
+ )
# Make new rule top priority
row = 0
@@ -184,7 +198,9 @@ def move_item_up(self, item_name: str) -> bool:
return False
with ConfigSnapshotUndoCommand(
- f"Move {self.item_type_label()}", model=self, item_name=item_name
+ f"Move {self.item_type_label()}",
+ model=self,
+ item_name=item_name,
):
file_rules.increaseRulePriority(src_rule_index)
ocio.GetCurrentConfig().setFileRules(file_rules)
@@ -217,7 +233,9 @@ def move_item_down(self, item_name: str) -> bool:
return False
with ConfigSnapshotUndoCommand(
- f"Move {self.item_type_label()}", model=self, item_name=item_name
+ f"Move {self.item_type_label()}",
+ model=self,
+ item_name=item_name,
):
file_rules.decreaseRulePriority(src_rule_index)
ocio.GetCurrentConfig().setFileRules(file_rules)
@@ -233,7 +251,9 @@ def get_item_names(self) -> list[str]:
config = ocio.GetCurrentConfig()
file_rules = config.getFileRules()
- return [file_rules.getName(i) for i in range(file_rules.getNumEntries())]
+ return [
+ file_rules.getName(i) for i in range(file_rules.getNumEntries())
+ ]
def _get_icon(
self, item: FileRule, column_desc: ColumnDesc
@@ -294,7 +314,9 @@ def _clear_items(self) -> None:
ocio.GetCurrentConfig().setFileRules(ocio.FileRules())
@staticmethod
- def _insert_rule(index: int, file_rules: ocio.FileRules, item: FileRule) -> None:
+ def _insert_rule(
+ index: int, file_rules: ocio.FileRules, item: FileRule
+ ) -> None:
"""
Insert rule into an ``ocio.FileRules`` object from a FileRule
instance.
@@ -308,7 +330,9 @@ def _insert_rule(index: int, file_rules: ocio.FileRules, item: FileRule) -> None
for key_name, key_value in item.custom_keys.items():
file_rules.setCustomKey(index, key_name, key_value)
- def _remove_named_rule(self, file_rules: ocio.ViewingRules, item: FileRule) -> None:
+ def _remove_named_rule(
+ self, file_rules: ocio.ViewingRules, item: FileRule
+ ) -> None:
"""Remove existing rule with name matching the provided rule."""
# Default rule can't be removed
if item.name != ocio.DEFAULT_RULE_NAME:
diff --git a/src/apps/ocioview/ocioview/items/look_edit.py b/src/apps/ocioview/ocioview/items/look_edit.py
index 630051c9ca..d815be1811 100644
--- a/src/apps/ocioview/ocioview/items/look_edit.py
+++ b/src/apps/ocioview/ocioview/items/look_edit.py
@@ -7,8 +7,7 @@
import PyOpenColorIO as ocio
from PySide6 import QtWidgets
-from ..config_cache import ConfigCache
-from ..widgets import CallbackComboBox, TextEdit
+from ..widgets import ColorSpaceComboBox, TextEdit
from .look_model import LookModel
from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit
@@ -27,8 +26,8 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent=parent)
# Widgets
- self.process_space_combo = CallbackComboBox(
- lambda: ConfigCache.get_color_space_names(ocio.SEARCH_REFERENCE_SPACE_SCENE)
+ self.process_space_combo = ColorSpaceComboBox(
+ ocio.SEARCH_REFERENCE_SPACE_SCENE, include_roles=True
)
self.description_edit = TextEdit()
@@ -36,7 +35,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
self._param_layout.addRow(
self.model.PROCESS_SPACE.label, self.process_space_combo
)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
class LookEdit(BaseConfigItemEdit):
@@ -52,16 +53,16 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.process_space_combo, model.PROCESS_SPACE.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
# Trigger immediate update from widgets that update the model upon losing focus
- self.param_edit.process_space_combo.currentIndexChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ self.param_edit.process_space_combo.color_space_changed.connect(
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
# Initialize
diff --git a/src/apps/ocioview/ocioview/items/look_model.py b/src/apps/ocioview/ocioview/items/look_model.py
index 780a950db2..b2e19372dc 100644
--- a/src/apps/ocioview/ocioview/items/look_model.py
+++ b/src/apps/ocioview/ocioview/items/look_model.py
@@ -40,7 +40,9 @@ def get_item_transforms(
# Get look name from subscription item label
item_name = self.extract_subscription_item_name(item_label)
- scene_ref_name = ReferenceSpaceManager.scene_reference_space().getName()
+ scene_ref_name = (
+ ReferenceSpaceManager.scene_reference_space().getName()
+ )
return (
ocio.LookTransform(
src=scene_ref_name,
@@ -59,9 +61,9 @@ def get_item_transforms(
def _get_icon(
self, item: ocio.ColorSpace, column_desc: ColumnDesc
) -> Optional[QtGui.QIcon]:
- return self._get_subscription_icon(item, column_desc) or super()._get_icon(
+ return self._get_subscription_icon(
item, column_desc
- )
+ ) or super()._get_icon(item, column_desc)
def _get_bg_color(
self, item: __item_type__, column_desc: ColumnDesc
@@ -130,7 +132,10 @@ def _get_value(self, item: ocio.Look, column_desc: ColumnDesc) -> Any:
# Process space is unset; find a reasonable default. Start with the most
# common roles for shot grades or ACES LMTs.
- for role in (ocio.ROLE_COLOR_TIMING, ocio.ROLE_INTERCHANGE_SCENE):
+ for role in (
+ ocio.ROLE_COLOR_TIMING,
+ ocio.ROLE_INTERCHANGE_SCENE,
+ ):
process_space = config.getCanonicalName(role)
if process_space:
break
@@ -195,7 +200,9 @@ def _set_value(
# Preserve item order when replacing item due to name change, which requires
# removing the old item to add the new.
if column_desc == self.NAME:
- items = [copy.deepcopy(other_item) for other_item in config.getLooks()]
+ items = [
+ copy.deepcopy(other_item) for other_item in config.getLooks()
+ ]
config.clearLooks()
for other_look in items:
if other_look.getName() == prev_item_name:
@@ -212,5 +219,6 @@ def _set_value(
if column_desc in (self.NAME, self.TRANSFORM, self.INVERSE_TRANSFORM):
item_name = new_item.getName()
self._update_tf_subscribers(
- item_name, prev_item_name if prev_item_name != item_name else None
+ item_name,
+ prev_item_name if prev_item_name != item_name else None,
)
diff --git a/src/apps/ocioview/ocioview/items/named_transform_edit.py b/src/apps/ocioview/ocioview/items/named_transform_edit.py
index fefe69f544..b70d9919af 100644
--- a/src/apps/ocioview/ocioview/items/named_transform_edit.py
+++ b/src/apps/ocioview/ocioview/items/named_transform_edit.py
@@ -30,23 +30,37 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
# Widgets
self.aliases_list = StringListWidget(
item_basename="alias",
- item_icon=get_glyph_icon("ph.bookmark-simple", size=ICON_SIZE_ITEM),
+ 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.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", size=ICON_SIZE_ITEM),
+ item_icon=get_glyph_icon(
+ "ph.bookmarks-simple", size=ICON_SIZE_ITEM
+ ),
get_presets=self._get_available_categories,
)
# Layout
self._param_layout.addRow(self.model.ALIASES.label, self.aliases_list)
self._param_layout.addRow(self.model.FAMILY.label, self.family_edit)
- self._param_layout.addRow(self.model.ENCODING.label, self.encoding_edit)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
- self._param_layout.addRow(self.model.CATEGORIES.label, self.categories_list)
+ self._param_layout.addRow(
+ self.model.ENCODING.label, self.encoding_edit
+ )
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
+ self._param_layout.addRow(
+ self.model.CATEGORIES.label, self.categories_list
+ )
def _get_available_categories(self) -> list[str]:
"""
@@ -54,7 +68,11 @@ def _get_available_categories(self) -> list[str]:
to this item.
"""
current_categories = self.categories_list.items()
- return [c for c in ConfigCache.get_categories() if c not in current_categories]
+ return [
+ c
+ for c in ConfigCache.get_categories()
+ if c not in current_categories
+ ]
class NamedTransformEdit(BaseConfigItemEdit):
@@ -70,19 +88,27 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(self.param_edit.aliases_list, model.ALIASES.column)
- self._mapper.addMapping(self.param_edit.family_edit, model.FAMILY.column)
- self._mapper.addMapping(self.param_edit.encoding_edit, model.ENCODING.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(
+ self.param_edit.aliases_list, model.ALIASES.column
+ )
+ self.mapper.addMapping(
+ self.param_edit.family_edit, model.FAMILY.column
+ )
+ self.mapper.addMapping(
+ self.param_edit.encoding_edit, model.ENCODING.column
+ )
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.categories_list, model.CATEGORIES.column
)
# list widgets need manual data submission back to model
- self.param_edit.aliases_list.items_changed.connect(self._mapper.submit)
- self.param_edit.categories_list.items_changed.connect(self._mapper.submit)
+ self.param_edit.aliases_list.items_changed.connect(self.mapper.submit)
+ self.param_edit.categories_list.items_changed.connect(
+ self.mapper.submit
+ )
# Initialize
if model.rowCount():
diff --git a/src/apps/ocioview/ocioview/items/named_transform_model.py b/src/apps/ocioview/ocioview/items/named_transform_model.py
index f42378438e..814e1ec854 100644
--- a/src/apps/ocioview/ocioview/items/named_transform_model.py
+++ b/src/apps/ocioview/ocioview/items/named_transform_model.py
@@ -40,7 +40,9 @@ 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", size=ICON_SIZE_ITEM)
+ 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()]
@@ -57,15 +59,23 @@ def get_item_transforms(
fwd_tf = named_transform.getTransform(ocio.TRANSFORM_DIR_FORWARD)
if not fwd_tf:
- inv_tf_ = named_transform.getTransform(ocio.TRANSFORM_DIR_INVERSE)
+ inv_tf_ = named_transform.getTransform(
+ ocio.TRANSFORM_DIR_INVERSE
+ )
if inv_tf_:
- fwd_tf = ocio.GroupTransform([inv_tf_], ocio.TRANSFORM_DIR_INVERSE)
+ fwd_tf = ocio.GroupTransform(
+ [inv_tf_], ocio.TRANSFORM_DIR_INVERSE
+ )
inv_tf = named_transform.getTransform(ocio.TRANSFORM_DIR_INVERSE)
if not inv_tf:
- fwd_tf_ = named_transform.getTransform(ocio.TRANSFORM_DIR_FORWARD)
+ fwd_tf_ = named_transform.getTransform(
+ ocio.TRANSFORM_DIR_FORWARD
+ )
if fwd_tf_:
- inv_tf = ocio.GroupTransform([fwd_tf_], ocio.TRANSFORM_DIR_INVERSE)
+ inv_tf = ocio.GroupTransform(
+ [fwd_tf_], ocio.TRANSFORM_DIR_INVERSE
+ )
return fwd_tf, inv_tf
else:
@@ -74,9 +84,9 @@ def get_item_transforms(
def _get_icon(
self, item: ocio.ColorSpace, column_desc: ColumnDesc
) -> Optional[QtGui.QIcon]:
- return self._get_subscription_icon(item, column_desc) or super()._get_icon(
+ return self._get_subscription_icon(
item, column_desc
- )
+ ) or super()._get_icon(item, column_desc)
def _get_bg_color(
self, item: __item_type__, column_desc: ColumnDesc
@@ -89,7 +99,8 @@ def _get_bg_color(
def _get_items(self, preserve: bool = False) -> list[ocio.ColorSpace]:
if preserve:
self._items = [
- copy.deepcopy(item) for item in ConfigCache.get_named_transforms()
+ copy.deepcopy(item)
+ for item in ConfigCache.get_named_transforms()
]
return self._items
else:
@@ -116,10 +127,14 @@ def _remove_item(self, item: ocio.NamedTransform) -> None:
def _new_item(self, name: str) -> None:
ocio.GetCurrentConfig().addNamedTransform(
- ocio.NamedTransform(name=name, forwardTransform=ocio.GroupTransform())
+ ocio.NamedTransform(
+ name=name, forwardTransform=ocio.GroupTransform()
+ )
)
- def _get_value(self, item: ocio.NamedTransform, column_desc: ColumnDesc) -> Any:
+ def _get_value(
+ self, item: ocio.NamedTransform, column_desc: ColumnDesc
+ ) -> Any:
# Get parameters
if column_desc == self.NAME:
return item.getName()
@@ -203,8 +218,13 @@ def _set_value(
config.addNamedTransform(new_item)
# Broadcast transform or name changes to subscribers
- if column_desc in (self.NAME, self.FORWARD_TRANSFORM, self.INVERSE_TRANSFORM):
+ if column_desc in (
+ self.NAME,
+ self.FORWARD_TRANSFORM,
+ self.INVERSE_TRANSFORM,
+ ):
item_name = new_item.getName()
self._update_tf_subscribers(
- item_name, prev_item_name if prev_item_name != item_name else None
+ item_name,
+ prev_item_name if prev_item_name != item_name else None,
)
diff --git a/src/apps/ocioview/ocioview/items/role_edit.py b/src/apps/ocioview/ocioview/items/role_edit.py
index f527ceef1e..56593de78c 100644
--- a/src/apps/ocioview/ocioview/items/role_edit.py
+++ b/src/apps/ocioview/ocioview/items/role_edit.py
@@ -5,6 +5,7 @@
from PySide6 import QtGui, QtWidgets
+from ..signal_router import SignalRouter
from ..widgets import ItemModelTableWidget
from .delegates import RoleDelegate
from .role_model import RoleModel
@@ -35,6 +36,24 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
self.model = RoleModel()
+ # Connect signal router to model change
+ signal_router = SignalRouter.get_instance()
+ self.model.dataChanged.connect(
+ lambda *a, **kw: signal_router.emit_roles_changed()
+ )
+ self.model.item_renamed.connect(
+ lambda *a, **kw: signal_router.emit_roles_changed()
+ )
+ self.model.item_added.connect(
+ lambda *a, **kw: signal_router.emit_roles_changed()
+ )
+ self.model.item_moved.connect(
+ lambda *a, **kw: signal_router.emit_roles_changed()
+ )
+ self.model.item_removed.connect(
+ lambda *a, **kw: signal_router.emit_roles_changed()
+ )
+
# Widgets
self.table = ItemModelTableWidget(self.model)
self.table.view.setItemDelegate(RoleDelegate(self.model))
diff --git a/src/apps/ocioview/ocioview/items/shared_view_edit.py b/src/apps/ocioview/ocioview/items/shared_view_edit.py
index db474e5316..1f93bf0644 100644
--- a/src/apps/ocioview/ocioview/items/shared_view_edit.py
+++ b/src/apps/ocioview/ocioview/items/shared_view_edit.py
@@ -8,7 +8,7 @@
from PySide6 import QtWidgets
from ..config_cache import ConfigCache
-from ..widgets import CallbackComboBox, LineEdit
+from ..widgets import CallbackComboBox, ColorSpaceComboBox, LineEdit
from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit
from .shared_view_model import SharedViewModel
@@ -23,25 +23,31 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent=parent)
# Widgets
- self.color_space_combo = CallbackComboBox(
- lambda: [ocio.OCIO_VIEW_USE_DISPLAY_NAME]
- + ConfigCache.get_color_space_names(ocio.SEARCH_REFERENCE_SPACE_DISPLAY)
+ self.color_space_combo = ColorSpaceComboBox(
+ reference_space_type=ocio.SEARCH_REFERENCE_SPACE_DISPLAY,
+ include_use_display_name=True,
)
self.view_transform_combo = CallbackComboBox(
ConfigCache.get_view_transform_names
)
self.looks_edit = LineEdit()
- self.rule_combo = CallbackComboBox(ConfigCache.get_viewing_rule_names)
+ self.rule_combo = CallbackComboBox(
+ lambda: [""] + ConfigCache.get_viewing_rule_names()
+ )
self.description_edit = LineEdit()
# Layout
- self._param_layout.addRow(self.model.COLOR_SPACE.label, self.color_space_combo)
+ self._param_layout.addRow(
+ self.model.COLOR_SPACE.label, self.color_space_combo
+ )
self._param_layout.addRow(
self.model.VIEW_TRANSFORM.label, self.view_transform_combo
)
self._param_layout.addRow(self.model.LOOKS.label, self.looks_edit)
self._param_layout.addRow(self.model.RULE.label, self.rule_combo)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
class SharedViewEdit(BaseConfigItemEdit):
@@ -57,24 +63,24 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.color_space_combo, model.COLOR_SPACE.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.view_transform_combo, model.VIEW_TRANSFORM.column
)
- self._mapper.addMapping(self.param_edit.looks_edit, model.LOOKS.column)
- self._mapper.addMapping(self.param_edit.rule_combo, model.RULE.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(self.param_edit.looks_edit, model.LOOKS.column)
+ self.mapper.addMapping(self.param_edit.rule_combo, model.RULE.column)
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
# Trigger immediate update from widgets that update the model upon losing focus
- self.param_edit.color_space_combo.currentIndexChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ self.param_edit.color_space_combo.color_space_changed.connect(
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
self.param_edit.view_transform_combo.currentIndexChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
# Initialize
diff --git a/src/apps/ocioview/ocioview/items/shared_view_model.py b/src/apps/ocioview/ocioview/items/shared_view_model.py
index 7769127781..e6ff4063a4 100644
--- a/src/apps/ocioview/ocioview/items/shared_view_model.py
+++ b/src/apps/ocioview/ocioview/items/shared_view_model.py
@@ -65,7 +65,9 @@ def _get_items(self, preserve: bool = False) -> list[SharedView]:
shared_view_display_map = defaultdict(set)
for display in ConfigCache.get_displays():
- for view in ConfigCache.get_views(display, view_type=ocio.VIEW_SHARED):
+ for view in ConfigCache.get_views(
+ display, view_type=ocio.VIEW_SHARED
+ ):
shared_view_display_map[view].add(display)
self._items = []
diff --git a/src/apps/ocioview/ocioview/items/utils.py b/src/apps/ocioview/ocioview/items/utils.py
index 24ab61256d..6beb6dde17 100644
--- a/src/apps/ocioview/ocioview/items/utils.py
+++ b/src/apps/ocioview/ocioview/items/utils.py
@@ -4,11 +4,15 @@
from __future__ import annotations
import enum
+import logging
from typing import Optional
import PyOpenColorIO as ocio
+logger = logging.getLogger(__name__)
+
+
class ViewType(str, enum.Enum):
"""Enum of view types."""
@@ -17,20 +21,17 @@ class ViewType(str, enum.Enum):
VIEW_SCENE = "View (Scene Reference Space)"
-def get_view_type(display: str, view: str) -> tuple[ViewType, str | None]:
+def get_view_type(display: str, view: str) -> ViewType:
"""
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: Tuple of view type and any warning raised while inspecting
- the display and view.
+ :return: View type
"""
- warning = None
-
if not display:
- return ViewType.VIEW_SHARED, warning
+ return ViewType.VIEW_SHARED
config = ocio.GetCurrentConfig()
@@ -39,24 +40,27 @@ def get_view_type(display: str, view: str) -> tuple[ViewType, str | None]:
color_space = config.getColorSpace(color_space_name)
if color_space is not None:
- if color_space.getReferenceSpaceType() == ocio.REFERENCE_SPACE_DISPLAY:
+ if color_space.getReferenceSpaceType() == ocio.REFERENCE_SPACE_SCENE:
if view_transform_name:
- warning = (
+ logger.warning(
f"Invalid view '{display}/{view}' references a view transform "
- f"('{view_transform_name}') with a non-display color space "
+ f"('{view_transform_name}') and 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
+ return ViewType.VIEW_SCENE
else:
- return ViewType.VIEW_SCENE, warning
+ return ViewType.VIEW_DISPLAY
+
elif view_transform_name:
- return ViewType.VIEW_DISPLAY, warning
+ return ViewType.VIEW_DISPLAY
else:
- return ViewType.VIEW_SCENE, warning
+ return ViewType.VIEW_SCENE
-def adapt_splitter_sizes(from_sizes: list[int], to_sizes: list[int]) -> list[int]:
+def adapt_splitter_sizes(
+ from_sizes: list[int], to_sizes: list[int]
+) -> list[int]:
"""
Given source and destination splitter size lists, adapt the
destination sizes to match the source sizes. Supports between two
diff --git a/src/apps/ocioview/ocioview/items/view_edit.py b/src/apps/ocioview/ocioview/items/view_edit.py
index fecd655f6a..da4ab9089a 100644
--- a/src/apps/ocioview/ocioview/items/view_edit.py
+++ b/src/apps/ocioview/ocioview/items/view_edit.py
@@ -11,6 +11,7 @@
from ..transform_manager import TransformManager
from ..widgets import (
CallbackComboBox,
+ ColorSpaceComboBox,
ExpandingStackedWidget,
FormLayout,
ItemModelListWidget,
@@ -57,7 +58,8 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
self.description_edits = {}
self.edit_shared_view_button = QtWidgets.QPushButton(
- self.model.get_view_type_icon(ViewType.VIEW_SHARED), "Edit Shared View"
+ self.model.get_view_type_icon(ViewType.VIEW_SHARED),
+ "Edit Shared View",
)
for view_type in self.VIEW_LAYERS:
@@ -77,40 +79,42 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
view_transform_combo = CallbackComboBox(
ConfigCache.get_view_transform_names
)
- self.view_transform_combos[view_type] = view_transform_combo
+ self.view_transform_combos[
+ view_type
+ ] = view_transform_combo
params_layout.addRow(
self.model.VIEW_TRANSFORM.label, view_transform_combo
)
if view_type == ViewType.VIEW_SCENE:
- get_view_color_space_names = (
- lambda: ConfigCache.get_color_space_names(
- ocio.SEARCH_REFERENCE_SPACE_SCENE
- )
+ color_space_combo = ColorSpaceComboBox(
+ reference_space_type=ocio.SEARCH_REFERENCE_SPACE_SCENE,
)
else: # ViewType.VIEW_DISPLAY
- get_view_color_space_names = (
- lambda: ConfigCache.get_color_space_names(
- ocio.SEARCH_REFERENCE_SPACE_DISPLAY
- )
+ color_space_combo = ColorSpaceComboBox(
+ reference_space_type=ocio.SEARCH_REFERENCE_SPACE_DISPLAY,
)
-
- color_space_combo = CallbackComboBox(get_view_color_space_names)
self.color_space_combos[view_type] = color_space_combo
- params_layout.addRow(self.model.COLOR_SPACE.label, color_space_combo)
+ params_layout.addRow(
+ self.model.COLOR_SPACE.label, color_space_combo
+ )
looks_edit = LineEdit()
self.looks_edits[view_type] = looks_edit
params_layout.addRow(self.model.LOOKS.label, looks_edit)
if view_type == ViewType.VIEW_DISPLAY:
- rule_combo = CallbackComboBox(ConfigCache.get_viewing_rule_names)
+ rule_combo = CallbackComboBox(
+ ConfigCache.get_viewing_rule_names
+ )
self.rule_combos[view_type] = rule_combo
params_layout.addRow(self.model.RULE.label, rule_combo)
description_edit = LineEdit()
self.description_edits[view_type] = description_edit
- params_layout.addRow(self.model.DESCRIPTION.label, description_edit)
+ params_layout.addRow(
+ self.model.DESCRIPTION.label, description_edit
+ )
elif view_type == ViewType.VIEW_SHARED:
params_layout.addRow(self.edit_shared_view_button)
@@ -146,19 +150,25 @@ def update_available_params(
)
if view_type in (ViewType.VIEW_DISPLAY, ViewType.VIEW_SCENE):
- view_mapper.addMapping(self.name_edits[view_type], self.model.NAME.column)
+ view_mapper.addMapping(
+ self.name_edits[view_type], self.model.NAME.column
+ )
color_space_combo = self.color_space_combos[view_type]
- view_mapper.addMapping(color_space_combo, self.model.COLOR_SPACE.column)
+ view_mapper.addMapping(
+ color_space_combo, self.model.COLOR_SPACE.column
+ )
# Trigger color space update before losing focus
if color_space_combo not in self._connected[view_mapper]:
- color_space_combo.currentIndexChanged.connect(
+ color_space_combo.color_space_changed.connect(
partial(self.submit_mapper_deferred, view_mapper)
)
self._connected[view_mapper].append(color_space_combo)
- view_mapper.addMapping(self.looks_edits[view_type], self.model.LOOKS.column)
+ view_mapper.addMapping(
+ self.looks_edits[view_type], self.model.LOOKS.column
+ )
if view_type == ViewType.VIEW_DISPLAY:
view_transform_combo = self.view_transform_combos[view_type]
@@ -178,7 +188,8 @@ def update_available_params(
self.rule_combos[view_type], self.model.RULE.column
)
view_mapper.addMapping(
- self.description_edits[view_type], self.model.DESCRIPTION.column
+ self.description_edits[view_type],
+ self.model.DESCRIPTION.column,
)
@@ -193,7 +204,9 @@ class ViewEdit(BaseConfigItemEdit):
@classmethod
def item_type_label(cls, plural: bool = False) -> str:
- return f"Display{'s' if plural else ''} and View{'s' if plural else ''}"
+ return (
+ f"Display{'s' if plural else ''} and View{'s' if plural else ''}"
+ )
def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent=parent)
@@ -210,7 +223,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
item_icon=DisplayModel.item_type_icon(),
)
self.display_list.current_row_changed.connect(self._on_display_changed)
- self.display_model.item_added.connect(self.display_list.set_current_item)
+ self.display_model.item_added.connect(
+ self.display_list.set_current_item
+ )
self.display_model.item_selection_requested.connect(
lambda index: self.display_list.set_current_row(index.row())
)
@@ -219,11 +234,15 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
# Map display widget to model
self._display_mapper = QtWidgets.QDataWidgetMapper()
self._display_mapper.setOrientation(QtCore.Qt.Horizontal)
- self._display_mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.ManualSubmit)
+ self._display_mapper.setSubmitPolicy(
+ QtWidgets.QDataWidgetMapper.ManualSubmit
+ )
self._display_mapper.setModel(self.display_model)
for view_type, display_edit in self.param_edit.display_edits.items():
- display_edit.editingFinished.connect(self._on_display_editing_finished)
+ display_edit.editingFinished.connect(
+ self._on_display_editing_finished
+ )
self.param_edit.edit_shared_view_button.released.connect(
self._on_edit_shared_view_button_clicked
@@ -231,7 +250,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
# Clear default mapped widgets from view model. Widgets will be remapped per
# view type.
- self._mapper.clearMapping()
+ self.mapper.clearMapping()
# Layout
self.splitter.insertWidget(0, self.display_list)
@@ -260,7 +279,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)
@@ -280,7 +299,9 @@ def _on_display_renamed(self, display: str, prev_display: str) -> None:
prev_item_label = self.model.format_subscription_item_label(
view_index, display=prev_display
)
- slot = TransformManager.get_subscription_slot(self.model, prev_item_label)
+ slot = TransformManager.get_subscription_slot(
+ self.model, prev_item_label
+ )
if slot != -1:
TransformManager.set_subscription(slot, self.model, item_label)
@@ -298,7 +319,9 @@ def _on_display_changed(self, display_row: int) -> None:
else:
# Get display and view names
display = self.display_model.data(
- self.display_model.index(display_row, self.display_model.NAME.column)
+ self.display_model.index(
+ display_row, self.display_model.NAME.column
+ )
)
# Update view model
@@ -332,7 +355,9 @@ def _on_current_row_changed(self, view_row: int) -> None:
view_type = None
if view_row != -1:
self._prev_view = self.model.data(
- self.model.index(self.list.current_row(), self.model.NAME.column)
+ self.model.index(
+ self.list.current_row(), self.model.NAME.column
+ )
)
view_type = self.model.data(
self.model.index(view_row, self.model.VIEW_TYPE.column),
@@ -341,11 +366,13 @@ def _on_current_row_changed(self, view_row: int) -> None:
# Update parameter widget states, since view type may have changed
self.param_edit.update_available_params(
- self._display_mapper, self._mapper, view_type
+ self._display_mapper, self.mapper, view_type
)
# Update display params
- self._display_mapper.setCurrentIndex(self._display_mapper.currentIndex())
+ self._display_mapper.setCurrentIndex(
+ self._display_mapper.currentIndex()
+ )
# Update view params
super()._on_current_row_changed(view_row)
diff --git a/src/apps/ocioview/ocioview/items/view_model.py b/src/apps/ocioview/ocioview/items/view_model.py
index e4602552f0..0af2350794 100644
--- a/src/apps/ocioview/ocioview/items/view_model.py
+++ b/src/apps/ocioview/ocioview/items/view_model.py
@@ -58,7 +58,9 @@ def requires_presets(cls) -> bool:
@classmethod
def get_presets(cls) -> Optional[Union[list[str], dict[str, QtGui.QIcon]]]:
presets = {
- ViewType.VIEW_DISPLAY: cls.get_view_type_icon(ViewType.VIEW_DISPLAY),
+ ViewType.VIEW_DISPLAY: cls.get_view_type_icon(
+ ViewType.VIEW_DISPLAY
+ ),
ViewType.VIEW_SCENE: cls.get_view_type_icon(ViewType.VIEW_SCENE),
}
for view in ConfigCache.get_shared_views():
@@ -72,8 +74,12 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self._display = None
self._view_type_icons = {
- ViewType.VIEW_SHARED: self.get_view_type_icon(ViewType.VIEW_SHARED),
- ViewType.VIEW_DISPLAY: self.get_view_type_icon(ViewType.VIEW_DISPLAY),
+ ViewType.VIEW_SHARED: self.get_view_type_icon(
+ ViewType.VIEW_SHARED
+ ),
+ ViewType.VIEW_DISPLAY: self.get_view_type_icon(
+ ViewType.VIEW_DISPLAY
+ ),
ViewType.VIEW_SCENE: self.get_view_type_icon(ViewType.VIEW_SCENE),
}
@@ -103,10 +109,15 @@ def add_preset(self, preset_name: str) -> int:
)
if color_spaces:
color_space = color_spaces[0]
- views = ConfigCache.get_views(view_type=ocio.VIEW_DISPLAY_DEFINED)
+ views = ConfigCache.get_views(
+ view_type=ocio.VIEW_DISPLAY_DEFINED
+ )
item = View(
- ViewType.VIEW_SCENE, next_name("View_", views), color_space, ""
+ ViewType.VIEW_SCENE,
+ next_name("View_", views),
+ color_space,
+ "",
)
else:
self.warning_raised.emit(
@@ -128,7 +139,9 @@ def add_preset(self, preset_name: str) -> int:
)
if color_spaces:
color_space = color_spaces[0]
- views = ConfigCache.get_views(view_type=ocio.VIEW_DISPLAY_DEFINED)
+ views = ConfigCache.get_views(
+ view_type=ocio.VIEW_DISPLAY_DEFINED
+ )
item = View(
ViewType.VIEW_DISPLAY,
@@ -158,7 +171,9 @@ def add_preset(self, preset_name: str) -> int:
if item is not None:
with ConfigSnapshotUndoCommand(
- f"Add {self.item_type_label()}", model=self, item_name=item.name
+ f"Add {self.item_type_label()}",
+ model=self,
+ item_name=item.name,
):
self._add_item(item)
row = self.get_item_names().index(item.name)
@@ -246,7 +261,9 @@ def get_item_transforms(
# Get view name from subscription item label
item_name = self.extract_subscription_item_name(item_label)
- scene_ref_name = ReferenceSpaceManager.scene_reference_space().getName()
+ scene_ref_name = (
+ ReferenceSpaceManager.scene_reference_space().getName()
+ )
return (
ocio.DisplayViewTransform(
src=scene_ref_name,
@@ -290,11 +307,14 @@ 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_label(item_name)})"
+ f"({item_name})",
+ f"({self.format_subscription_item_label(item_name)})",
)
return text
- def _get_icon(self, item: View, column_desc: ColumnDesc) -> Optional[QtGui.QIcon]:
+ def _get_icon(
+ self, item: View, column_desc: ColumnDesc
+ ) -> Optional[QtGui.QIcon]:
if column_desc == self.NAME:
return (
self._get_subscription_icon(item, column_desc)
@@ -336,7 +356,7 @@ def _get_placeholder_view(self) -> View:
)
)
- return View(ViewType.VIEW_SCENE, "_", color_space, "")
+ return View(ViewType.VIEW_SCENE, "_", color_space)
def _reset_cache(self) -> None:
self._items = []
@@ -354,12 +374,19 @@ 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)
+ view_type = get_view_type(self._display, name)
+
+ if view_type == ViewType.VIEW_SCENE:
+ view = View(
+ view_type,
+ name,
+ config.getDisplayViewColorSpaceName(self._display, name),
+ looks=config.getDisplayViewLooks(self._display, name),
+ )
+ self._items.append(view)
- self._items.append(
- View(
+ else: # VIEW_DISPLAY
+ view = View(
view_type,
name,
config.getDisplayViewColorSpaceName(self._display, name),
@@ -368,21 +395,20 @@ def _get_items(self, preserve: bool = False) -> list[View]:
config.getDisplayViewRule(self._display, name),
config.getDisplayViewDescription(self._display, name),
)
- )
+ self._items.append(view)
# Shared views
for name in config.getViews(ocio.VIEW_SHARED, self._display):
- self._items.append(
- View(
- ViewType.VIEW_SHARED,
- name,
- config.getDisplayViewColorSpaceName("", name),
- config.getDisplayViewTransformName("", name),
- config.getDisplayViewLooks("", name),
- config.getDisplayViewRule("", name),
- config.getDisplayViewDescription("", name),
- )
+ view = View(
+ ViewType.VIEW_SHARED,
+ name,
+ config.getDisplayViewColorSpaceName("", name),
+ config.getDisplayViewTransformName("", name),
+ config.getDisplayViewLooks("", name),
+ config.getDisplayViewRule("", name),
+ config.getDisplayViewDescription("", name),
)
+ self._items.append(view)
return self._items
@@ -393,7 +419,9 @@ def _clear_items(self) -> None:
# Insert placeholder view to keep display alive
placeholder_view = self._get_placeholder_view()
config.addDisplayView(
- self._display, placeholder_view.name, placeholder_view.color_space
+ self._display,
+ placeholder_view.name,
+ placeholder_view.color_space,
)
# Views must be removed in reverse to preserve internal indices
@@ -461,7 +489,11 @@ def _get_value(self, item: View, column_desc: ColumnDesc) -> Any:
return None
def _set_value(
- self, item: View, column_desc: ColumnDesc, value: Any, index: QtCore.QModelIndex
+ self,
+ item: View,
+ column_desc: ColumnDesc,
+ value: Any,
+ index: QtCore.QModelIndex,
) -> None:
item_names = self.get_item_names()
if item.name not in item_names:
@@ -479,22 +511,21 @@ def _set_value(
color_space = config.getColorSpace(value)
if color_space:
if (
- item.view_transform
- and (
- color_space.getReferenceSpaceType()
- == ocio.REFERENCE_SPACE_DISPLAY
- )
- ) or (
- not item.view_transform
+ item.type == ViewType.VIEW_SCENE
and color_space.getReferenceSpaceType()
== ocio.REFERENCE_SPACE_SCENE
+ ) or (
+ item.type == ViewType.VIEW_DISPLAY
+ and color_space.getReferenceSpaceType()
+ == ocio.REFERENCE_SPACE_DISPLAY
):
items[item_index].color_space = value
elif column_desc == self.VIEW_TRANSFORM:
color_space = config.getColorSpace(item.color_space)
if color_space and (
- color_space.getReferenceSpaceType() == ocio.REFERENCE_SPACE_DISPLAY
+ color_space.getReferenceSpaceType()
+ == ocio.REFERENCE_SPACE_DISPLAY
):
items[item_index].view_transform = value
diff --git a/src/apps/ocioview/ocioview/items/view_transform_edit.py b/src/apps/ocioview/ocioview/items/view_transform_edit.py
index 8d0ac15cd4..b200faf600 100644
--- a/src/apps/ocioview/ocioview/items/view_transform_edit.py
+++ b/src/apps/ocioview/ocioview/items/view_transform_edit.py
@@ -10,7 +10,12 @@
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 ..widgets import (
+ EnumComboBox,
+ CallbackComboBox,
+ StringListWidget,
+ TextEdit,
+)
from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit
from .view_transform_model import ViewTransformModel
@@ -41,21 +46,30 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
),
},
)
- self.family_edit = CallbackComboBox(ConfigCache.get_families, editable=True)
+ 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", size=ICON_SIZE_ITEM),
+ item_icon=get_glyph_icon(
+ "ph.bookmarks-simple", size=ICON_SIZE_ITEM
+ ),
get_presets=self._get_available_categories,
)
# Layout
self._param_layout.addRow(
- self.model.REFERENCE_SPACE_TYPE.label, self.reference_space_type_combo
+ self.model.REFERENCE_SPACE_TYPE.label,
+ self.reference_space_type_combo,
)
self._param_layout.addRow(self.model.FAMILY.label, self.family_edit)
- self._param_layout.addRow(self.model.DESCRIPTION.label, self.description_edit)
- self._param_layout.addRow(self.model.CATEGORIES.label, self.categories_list)
+ self._param_layout.addRow(
+ self.model.DESCRIPTION.label, self.description_edit
+ )
+ self._param_layout.addRow(
+ self.model.CATEGORIES.label, self.categories_list
+ )
def _get_available_categories(self) -> list[str]:
"""
@@ -63,7 +77,11 @@ def _get_available_categories(self) -> list[str]:
to this item.
"""
current_categories = self.categories_list.items()
- return [c for c in ConfigCache.get_categories() if c not in current_categories]
+ return [
+ c
+ for c in ConfigCache.get_categories()
+ if c not in current_categories
+ ]
class ViewTransformEdit(BaseConfigItemEdit):
@@ -79,24 +97,28 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.reference_space_type_combo,
model.REFERENCE_SPACE_TYPE.column,
)
- self._mapper.addMapping(self.param_edit.family_edit, model.FAMILY.column)
- self._mapper.addMapping(
+ self.mapper.addMapping(
+ self.param_edit.family_edit, model.FAMILY.column
+ )
+ self.mapper.addMapping(
self.param_edit.description_edit, model.DESCRIPTION.column
)
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.categories_list, model.CATEGORIES.column
)
# list widgets need manual data submission back to model
- self.param_edit.categories_list.items_changed.connect(self._mapper.submit)
+ self.param_edit.categories_list.items_changed.connect(
+ self.mapper.submit
+ )
# Trigger immediate update from widgets that update the model upon losing focus
self.param_edit.reference_space_type_combo.currentIndexChanged.connect(
- partial(self.param_edit.submit_mapper_deferred, self._mapper)
+ partial(self.param_edit.submit_mapper_deferred, self.mapper)
)
# Initialize
diff --git a/src/apps/ocioview/ocioview/items/view_transform_model.py b/src/apps/ocioview/ocioview/items/view_transform_model.py
index 10f8d5ea22..82593dc489 100644
--- a/src/apps/ocioview/ocioview/items/view_transform_model.py
+++ b/src/apps/ocioview/ocioview/items/view_transform_model.py
@@ -11,7 +11,10 @@
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
+from .utils import (
+ get_scene_to_display_transform,
+ get_display_to_scene_transform,
+)
class ViewTransformModel(BaseConfigItemModel):
@@ -41,7 +44,9 @@ 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", size=ICON_SIZE_ITEM),
+ 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
),
@@ -88,7 +93,8 @@ def _get_bg_color(
def _get_items(self, preserve: bool = False) -> list[ocio.ViewTransform]:
if preserve:
self._items = [
- copy.deepcopy(item) for item in ConfigCache.get_view_transforms()
+ copy.deepcopy(item)
+ for item in ConfigCache.get_view_transforms()
]
return self._items
else:
@@ -123,7 +129,9 @@ def _new_item(self, name: str) -> None:
)
)
- def _get_value(self, item: ocio.ViewTransform, column_desc: ColumnDesc) -> Any:
+ def _get_value(
+ self, item: ocio.ViewTransform, column_desc: ColumnDesc
+ ) -> Any:
# Get parameters
if column_desc == self.REFERENCE_SPACE_TYPE:
return int(item.getReferenceSpaceType().value)
@@ -167,7 +175,9 @@ def _set_value(
name=item.getName(),
family=item.getFamily(),
description=item.getDescription(),
- toReference=item.getTransform(ocio.VIEWTRANSFORM_DIR_TO_REFERENCE),
+ toReference=item.getTransform(
+ ocio.VIEWTRANSFORM_DIR_TO_REFERENCE
+ ),
fromReference=item.getTransform(
ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE
),
@@ -203,7 +213,8 @@ def _set_value(
# type change, which requires removing the old item to add the new.
if column_desc in (self.REFERENCE_SPACE_TYPE, self.NAME):
items = [
- copy.deepcopy(other_item) for other_item in config.getViewTransforms()
+ copy.deepcopy(other_item)
+ for other_item in config.getViewTransforms()
]
config.clearViewTransforms()
for other_item in items:
@@ -223,5 +234,6 @@ def _set_value(
if column_desc in (self.NAME, self.TO_REFERENCE, self.FROM_REFERENCE):
item_name = new_item.getName()
self._update_tf_subscribers(
- item_name, prev_item_name if prev_item_name != item_name else None
+ item_name,
+ prev_item_name if prev_item_name != item_name else None,
)
diff --git a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py b/src/apps/ocioview/ocioview/items/viewing_rule_edit.py
index 31aed1bf79..fc72e28407 100644
--- a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py
+++ b/src/apps/ocioview/ocioview/items/viewing_rule_edit.py
@@ -73,8 +73,12 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
params_layout_a = FormLayout()
params_layout_a.setContentsMargins(0, 0, 0, 0)
params_layout_a.addRow(self.model.NAME.label, self.name_edit_a)
- params_layout_a.addRow(self.model.COLOR_SPACES.label, self.color_space_list)
- params_layout_a.addRow(self.model.CUSTOM_KEYS.label, self.custom_keys_table_a)
+ params_layout_a.addRow(
+ self.model.COLOR_SPACES.label, self.color_space_list
+ )
+ params_layout_a.addRow(
+ self.model.CUSTOM_KEYS.label, self.custom_keys_table_a
+ )
params_a = QtWidgets.QFrame()
params_a.setLayout(params_layout_a)
@@ -82,7 +86,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
params_layout_b.setContentsMargins(0, 0, 0, 0)
params_layout_b.addRow(self.model.NAME.label, self.name_edit_b)
params_layout_b.addRow(self.model.ENCODINGS.label, self.encoding_list)
- params_layout_b.addRow(self.model.CUSTOM_KEYS.label, self.custom_keys_table_b)
+ params_layout_b.addRow(
+ self.model.CUSTOM_KEYS.label, self.custom_keys_table_b
+ )
params_b = QtWidgets.QFrame()
params_b.setLayout(params_layout_b)
@@ -101,19 +107,25 @@ def reset(self) -> None:
self._param_stack.setCurrentIndex(0)
def update_available_params(
- self, mapper: QtWidgets.QDataWidgetMapper, viewing_rule_type: ViewingRuleType
+ self,
+ mapper: QtWidgets.QDataWidgetMapper,
+ viewing_rule_type: ViewingRuleType,
) -> None:
"""
Map and show the interface needed to edit this rule's type.
"""
if viewing_rule_type == ViewingRuleType.RULE_COLOR_SPACE:
mapper.addMapping(self.name_edit_a, self.model.NAME.column)
- mapper.addMapping(self.custom_keys_table_a, self.model.CUSTOM_KEYS.column)
+ mapper.addMapping(
+ self.custom_keys_table_a, self.model.CUSTOM_KEYS.column
+ )
self._param_stack.setCurrentIndex(1)
else: # ViewingRuleType.RULE_ENCODING
mapper.addMapping(self.name_edit_b, self.model.NAME.column)
- mapper.addMapping(self.custom_keys_table_b, self.model.CUSTOM_KEYS.column)
+ mapper.addMapping(
+ self.custom_keys_table_b, self.model.CUSTOM_KEYS.column
+ )
self._param_stack.setCurrentIndex(2)
def _on_item_removed(self) -> None:
@@ -135,16 +147,24 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
model = self.model
# Map widgets to model columns
- self._mapper.addMapping(
+ self.mapper.addMapping(
self.param_edit.color_space_list, model.COLOR_SPACES.column
)
- self._mapper.addMapping(self.param_edit.encoding_list, model.ENCODINGS.column)
+ self.mapper.addMapping(
+ self.param_edit.encoding_list, model.ENCODINGS.column
+ )
# list and table widgets need manual data submission back to model
- self.param_edit.color_space_list.items_changed.connect(self._mapper.submit)
- self.param_edit.encoding_list.items_changed.connect(self._mapper.submit)
- self.param_edit.custom_keys_table_a.items_changed.connect(self._mapper.submit)
- self.param_edit.custom_keys_table_b.items_changed.connect(self._mapper.submit)
+ self.param_edit.color_space_list.items_changed.connect(
+ self.mapper.submit
+ )
+ self.param_edit.encoding_list.items_changed.connect(self.mapper.submit)
+ self.param_edit.custom_keys_table_a.items_changed.connect(
+ self.mapper.submit
+ )
+ self.param_edit.custom_keys_table_b.items_changed.connect(
+ self.mapper.submit
+ )
# Initialize
if model.rowCount():
@@ -159,6 +179,8 @@ def _on_current_row_changed(self, row: int) -> None:
self.model.index(row, self.model.VIEWING_RULE_TYPE.column),
QtCore.Qt.EditRole,
)
- self.param_edit.update_available_params(self._mapper, viewing_rule_type)
+ self.param_edit.update_available_params(
+ self.mapper, viewing_rule_type
+ )
super()._on_current_row_changed(row)
diff --git a/src/apps/ocioview/ocioview/main_window.py b/src/apps/ocioview/ocioview/main_window.py
index c14a0100d9..c68d8d6bb6 100644
--- a/src/apps/ocioview/ocioview/main_window.py
+++ b/src/apps/ocioview/ocioview/main_window.py
@@ -14,10 +14,14 @@
from .constants import ICON_PATH_OCIO
from .inspect_dock import InspectDock
from .message_router import MessageRouter
+from .mode import OCIOViewMode
from .ref_space_manager import ReferenceSpaceManager
+from .signal_router import SignalRouter
from .settings import settings
from .undo import undo_stack
+from .utils import get_glyph_icon, SignalsBlocked
from .viewer_dock import ViewerDock
+from .widgets import EnumComboBox
logger = logging.getLogger(__name__)
@@ -42,16 +46,20 @@ class OCIOView(QtWidgets.QMainWindow):
def __init__(
self,
config_path: Optional[Path] = None,
+ transient: bool = False,
parent: Optional[QtCore.QObject] = None,
):
"""
:param config_path: Optional OCIO config path to load. Defaults
to the builtin raw config.
+ :param transient: Set to True to prevent any save operations,
+ making all config edits temporary.
"""
super().__init__(parent=parent)
self._config_path = None
self._config_save_cache_id = None
+ self._transient = transient
# Configure window
self.setWindowIcon(QtGui.QIcon(str(ICON_PATH_OCIO)))
@@ -60,9 +68,31 @@ def __init__(
self.recent_configs_menu = QtWidgets.QMenu("Load Recent Config")
self.recent_images_menu = QtWidgets.QMenu("Load Recent Image")
+ # Mode switcher
+ self.mode_box = EnumComboBox(
+ OCIOViewMode,
+ icons={
+ m: get_glyph_icon(m.value)
+ for m in OCIOViewMode.__members__.values()
+ },
+ )
+ self.mode_box.setToolTip("Application Mode")
+ self.mode_box.setMinimumContentsLength(
+ max(map(len, OCIOViewMode.__members__.keys()))
+ )
+ self.mode_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
+ self.mode_box.setSizePolicy(
+ QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed
+ )
+ )
+ self.mode_box.currentIndexChanged[int].connect(
+ self._on_mode_box_index_changed
+ )
+
# Dock widgets
self.inspect_dock = InspectDock()
- self.config_dock = ConfigDock()
+ self.config_dock = ConfigDock(corner_widget=self.mode_box)
# Central widget
self.viewer_dock = ViewerDock(self.recent_images_menu)
@@ -72,21 +102,30 @@ def __init__(
self.file_menu.addAction("New Config", self.new_config)
self.file_menu.addAction("Load Config...", self.load_config)
self.file_menu.addMenu(self.recent_configs_menu)
- self.file_menu.addAction(
- "Save config", self.save_config, QtGui.QKeySequence("Ctrl+S")
- )
- self.file_menu.addAction(
- "Save Config As...", self.save_config_as, QtGui.QKeySequence("Ctrl+Shift+S")
- )
- self.file_menu.addAction(
- "Save and Backup Config",
- self.save_and_backup_config,
- QtGui.QKeySequence("Ctrl+Alt+S"),
- )
- self.file_menu.addAction("Restore Config Backup...", self.restore_config_backup)
+
+ if not self._transient:
+ self.file_menu.addAction(
+ "Save config", self.save_config, QtGui.QKeySequence("Ctrl+S")
+ )
+ self.file_menu.addAction(
+ "Save Config As...",
+ self.save_config_as,
+ QtGui.QKeySequence("Ctrl+Shift+S"),
+ )
+ self.file_menu.addAction(
+ "Save and Backup Config",
+ self.save_and_backup_config,
+ QtGui.QKeySequence("Ctrl+Alt+S"),
+ )
+ self.file_menu.addAction(
+ "Restore Config Backup...", self.restore_config_backup
+ )
+
self.file_menu.addSeparator()
self.file_menu.addAction(
- "Load Image...", self.viewer_dock.load_image, QtGui.QKeySequence("Ctrl+I")
+ "Load Image...",
+ lambda: self.viewer_dock.load_image(),
+ QtGui.QKeySequence("Ctrl+I"),
)
self.file_menu.addMenu(self.recent_images_menu)
self.file_menu.addAction(
@@ -95,7 +134,9 @@ def __init__(
QtGui.QKeySequence("Ctrl+Shift+I"),
)
self.file_menu.addSeparator()
- self.file_menu.addAction("Exit", self.close, QtGui.QKeySequence("Ctrl+X"))
+ self.file_menu.addAction(
+ "Exit", self.close, QtGui.QKeySequence("Ctrl+X")
+ )
self.edit_menu = QtWidgets.QMenu("Edit")
undo_action = undo_stack.createUndoAction(self.edit_menu)
@@ -116,9 +157,15 @@ def __init__(
QtWidgets.QMainWindow.ForceTabbedDocks
| QtWidgets.QMainWindow.GroupedDragging
)
- self.setTabPosition(QtCore.Qt.BottomDockWidgetArea, QtWidgets.QTabWidget.North)
- self.setTabPosition(QtCore.Qt.LeftDockWidgetArea, QtWidgets.QTabWidget.North)
- self.setTabPosition(QtCore.Qt.RightDockWidgetArea, QtWidgets.QTabWidget.North)
+ self.setTabPosition(
+ QtCore.Qt.BottomDockWidgetArea, QtWidgets.QTabWidget.North
+ )
+ self.setTabPosition(
+ QtCore.Qt.LeftDockWidgetArea, QtWidgets.QTabWidget.North
+ )
+ self.setTabPosition(
+ QtCore.Qt.RightDockWidgetArea, QtWidgets.QTabWidget.North
+ )
for corner in (QtCore.Qt.TopLeftCorner, QtCore.Qt.BottomLeftCorner):
self.setCorner(corner, QtCore.Qt.LeftDockWidgetArea)
@@ -131,8 +178,11 @@ def __init__(
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.config_dock)
# Connections
- self.config_dock.config_changed.connect(self.viewer_dock.update_current_viewer)
- self.config_dock.config_changed.connect(self._update_window_title)
+ signal_router = SignalRouter.get_instance()
+ signal_router.config_changed.connect(
+ lambda: self.viewer_dock.update_current_viewer()
+ )
+ signal_router.config_reloaded.connect(self._update_window_title)
# Restore settings
settings.beginGroup(self.__class__.__name__)
@@ -141,16 +191,21 @@ def __init__(
if settings.contains(self.SETTING_STATE):
# If the version is not recognized, the restore will be bypassed
self.restoreState(
- settings.value(self.SETTING_STATE), version=self.SETTING_STATE_VERSION
+ settings.value(self.SETTING_STATE),
+ version=self.SETTING_STATE_VERSION,
)
settings.endGroup()
# Initialize
+ SignalRouter.get_instance().mode_changed.connect(
+ self._on_mode_changed_external
+ )
+
if config_path is not None:
self.load_config(config_path)
else:
# New config
- self._init_config_tracking()
+ self.new_config()
self._update_recent_configs_menu()
self._update_window_title()
@@ -165,18 +220,20 @@ def reset(self) -> None:
self._init_config_tracking()
self.config_dock.reset()
- self.viewer_dock.reset()
self.inspect_dock.reset()
+ self.viewer_dock.reset()
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
if self._can_close_config():
# Save settings
- settings.beginGroup(self.__class__.__name__)
- settings.setValue(self.SETTING_GEOMETRY, self.saveGeometry())
- settings.setValue(
- self.SETTING_STATE, self.saveState(self.SETTING_STATE_VERSION)
- )
- settings.endGroup()
+ if not self._transient:
+ settings.beginGroup(self.__class__.__name__)
+ settings.setValue(self.SETTING_GEOMETRY, self.saveGeometry())
+ settings.setValue(
+ self.SETTING_STATE,
+ self.saveState(self.SETTING_STATE_VERSION),
+ )
+ settings.endGroup()
event.accept()
super().closeEvent(event)
@@ -187,8 +244,12 @@ def new_config(self) -> None:
"""
Create and load a new OCIO raw config.
"""
- if not self._can_close_config():
- return
+ if (
+ self._config_path is not None
+ or self._config_save_cache_id is not None
+ ):
+ if not self._can_close_config():
+ return
self._config_path = None
self._config_save_cache_id = None
@@ -197,6 +258,8 @@ def new_config(self) -> None:
ocio.SetCurrentConfig(config)
self.reset()
+ SignalRouter.get_instance().emit_config_reloaded()
+
def load_config(self, config_path: Optional[Path] = None) -> None:
"""
Load a user specified OCIO config.
@@ -208,19 +271,29 @@ def load_config(self, config_path: Optional[Path] = None) -> None:
if config_path is None or not config_path.is_file():
config_dir = self._get_config_dir(config_path)
- config_path_str, file_filter = QtWidgets.QFileDialog.getOpenFileName(
- self, "Load Config", dir=config_dir, filter="OCIO Config (*.ocio)"
+ (
+ config_path_str,
+ file_filter,
+ ) = QtWidgets.QFileDialog.getOpenFileName(
+ self,
+ "Load Config",
+ dir=config_dir,
+ filter="OCIO Config (*.ocio)",
)
if not config_path_str:
return
config_path = Path(config_path_str)
- settings.setValue(self.SETTING_CONFIG_DIR, config_path.parent.as_posix())
+ if not self._transient:
+ settings.setValue(
+ self.SETTING_CONFIG_DIR, config_path.parent.as_posix()
+ )
self._config_path = config_path
# Add path to recent config files
- self._add_recent_config_path(self._config_path)
+ if not self._transient:
+ self._add_recent_config_path(self._config_path)
# Reset application with empty config to clean all components
config = ocio.Config()
@@ -232,6 +305,8 @@ def load_config(self, config_path: Optional[Path] = None) -> None:
ocio.SetCurrentConfig(config)
self.reset()
+ SignalRouter.get_instance().emit_config_reloaded()
+
def save_config(self) -> bool:
"""
Save the current OCIO config to the previously loaded config
@@ -240,7 +315,9 @@ def save_config(self) -> bool:
:return: Whether config was saved
"""
- if self._config_path is None:
+ if self._transient:
+ return False
+ elif self._config_path is None:
return self.save_config_as()
else:
try:
@@ -268,10 +345,16 @@ def save_config_as(self, config_path: Optional[Path] = None) -> bool:
:param config_path: Config file path
:return: Whether config was saved
"""
+ if self._transient:
+ return False
+
try:
if config_path is None or not config_path.is_file():
config_dir = self._get_config_dir(config_path)
- config_path_str, file_filter = QtWidgets.QFileDialog.getSaveFileName(
+ (
+ config_path_str,
+ file_filter,
+ ) = QtWidgets.QFileDialog.getSaveFileName(
self,
"Save Config",
dir=config_dir,
@@ -310,7 +393,10 @@ def save_and_backup_config(self) -> bool:
"""
if self.save_config():
try:
- if self._config_path is not None and self._config_path.is_file():
+ if (
+ self._config_path is not None
+ and self._config_path.is_file()
+ ):
next_version_path = self._get_next_version_path()
shutil.copy2(self._config_path, next_version_path)
return True
@@ -335,7 +421,10 @@ def restore_config_backup(self) -> None:
backup_dir = self._get_backup_dir()
if backup_dir is not None:
- version_path_str, file_filter = QtWidgets.QFileDialog.getOpenFileName(
+ (
+ version_path_str,
+ file_filter,
+ ) = QtWidgets.QFileDialog.getOpenFileName(
self,
"Restore Config",
dir=backup_dir.as_posix(),
@@ -368,7 +457,9 @@ def _get_next_version_path(self) -> Optional[Path]:
return None
max_version = 0
- for other_version_path in backup_dir.glob(self._format_version_filename()):
+ for other_version_path in backup_dir.glob(
+ self._format_version_filename()
+ ):
if other_version_path.is_file() and other_version_path.suffixes:
other_version_str = other_version_path.suffixes[0].strip(".")
if other_version_str.isdigit():
@@ -434,7 +525,9 @@ def _get_recent_config_paths(self) -> list[Path]:
num_configs = settings.beginReadArray(self.SETTING_RECENT_CONFIGS)
for i in range(num_configs):
settings.setArrayIndex(i)
- recent_config_path_str = settings.value(self.SETTING_RECENT_CONFIG_PATH)
+ recent_config_path_str = settings.value(
+ self.SETTING_RECENT_CONFIG_PATH
+ )
if recent_config_path_str:
recent_config_path = Path(recent_config_path_str)
if recent_config_path.is_file():
@@ -480,7 +573,9 @@ def _update_recent_configs_menu(self) -> None:
def _update_window_title(self) -> None:
filename = (
- "untitiled" if self._config_path is None else self._config_path.name
+ "untitiled"
+ if self._config_path is None
+ else self._config_path.name
) + ("*" if self._has_unsaved_changes() else "")
self.setWindowTitle(f"ocioview {ocio.__version__} | {filename}")
@@ -500,6 +595,9 @@ def _has_unsaved_changes(self) -> bool:
:return: Whether the current config has unsaved changes, when
compared to the previously saved config state.
"""
+ if self._transient:
+ return False
+
config_cache_id, is_valid = ConfigCache.get_cache_id()
return not is_valid or config_cache_id != self._config_save_cache_id
@@ -540,3 +638,18 @@ def _init_config_tracking(self) -> None:
"""Setup app-dependent config objects and change tracking."""
ReferenceSpaceManager.init_reference_spaces()
self._update_cache_id()
+
+ @QtCore.Slot(int)
+ def _on_mode_box_index_changed(self, index: int) -> None:
+ """Called when the application mode has been manually changed."""
+ with SignalsBlocked(self.mode_box):
+ OCIOViewMode.set_current_mode(self.mode_box.member())
+
+ def _on_mode_changed_external(self) -> None:
+ """
+ Called when the application mode has been changed externally.
+ """
+ with SignalsBlocked(self.mode_box):
+ mode = OCIOViewMode.current_mode()
+ if mode != self.mode_box.member():
+ self.mode_box.set_member(mode)
diff --git a/src/apps/ocioview/ocioview/message_router.py b/src/apps/ocioview/ocioview/message_router.py
index a18da91260..402d940a9f 100644
--- a/src/apps/ocioview/ocioview/message_router.py
+++ b/src/apps/ocioview/ocioview/message_router.py
@@ -14,7 +14,11 @@
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
+from .utils import (
+ config_to_html,
+ processor_to_ctf_html,
+ processor_to_shader_html,
+)
# Global message queue
@@ -191,7 +195,7 @@ def start_routing(self) -> None:
# Python or OCIO log record
else:
- self._handle_log_message(str(msg_raw))
+ self._handle_log_message(msg_raw)
self._is_routing = False
@@ -206,7 +210,9 @@ def _handle_config_message(self, config: ocio.Config) -> None:
self.config_html_ready.emit(config_html_data)
except Exception as e:
# Pass error to log
- self._handle_log_message(str(e), force_level=self.LOG_LEVEL_WARNING)
+ self._handle_log_message(
+ str(e), force_level=self.LOG_LEVEL_WARNING
+ )
def _handle_processor_message(
self,
@@ -221,7 +227,9 @@ def _handle_processor_message(
"""
try:
if self._processor_updates_allowed:
- self.processor_ready.emit(proc_context, proc.getDefaultCPUProcessor())
+ self.processor_ready.emit(
+ proc_context, proc.getDefaultCPUProcessor()
+ )
if self._ctf_updates_allowed:
ctf_html_data, group_tf = processor_to_ctf_html(proc)
@@ -236,7 +244,9 @@ def _handle_processor_message(
except Exception as e:
# Pass error to log
- self._handle_log_message(str(e), force_level=self.LOG_LEVEL_WARNING)
+ self._handle_log_message(
+ str(e), force_level=self.LOG_LEVEL_WARNING
+ )
def _handle_image_message(self, image_array: np.ndarray) -> None:
"""
@@ -248,7 +258,9 @@ def _handle_image_message(self, image_array: np.ndarray) -> None:
self.image_ready.emit(image_array)
except Exception as e:
# Pass error to log
- self._handle_log_message(str(e), force_level=self.LOG_LEVEL_WARNING)
+ self._handle_log_message(
+ str(e), force_level=self.LOG_LEVEL_WARNING
+ )
def _handle_log_message(
self, log_record: str, force_level: Optional[str] = None
@@ -330,7 +342,14 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self._thread = QtCore.QThread()
self._runner = MessageRunner()
self._runner.moveToThread(self._thread)
- self._thread.started.connect(self._runner.start_routing)
+
+ # Delay router start to ease application startup
+ self._timer = QtCore.QTimer()
+ self._timer.setSingleShot(True)
+ self._timer.setInterval(int(MessageRunner.LOOP_INTERVAL * 1000))
+ self._timer.timeout.connect(self._runner.start_routing)
+
+ self._thread.started.connect(self._timer.start)
# Make sure thread stops and routing is cleaned up on app close
app = QtWidgets.QApplication.instance()
diff --git a/src/apps/ocioview/ocioview/mode.py b/src/apps/ocioview/ocioview/mode.py
new file mode 100644
index 0000000000..4bf08bb11f
--- /dev/null
+++ b/src/apps/ocioview/ocioview/mode.py
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright Contributors to the OpenColorIO Project.
+
+from __future__ import annotations
+
+import enum
+
+
+class OCIOViewMode(enum.Enum):
+ """
+ ocioview application mode enum.
+
+ This class also manages the current application mode, which
+ components will refer to for mode-specific layout and behavior.
+ """
+
+ Edit = "mdi6.file-document-edit-outline"
+ """Mode for editing and inspecting an OCIO config."""
+
+ Preview = "mdi6.television-play"
+ """
+ Mode for previewing an OCIO config's user experience in a reference
+ integration.
+ """
+
+ __current = None
+ """Current application mode."""
+
+ @classmethod
+ def current_mode(cls) -> OCIOViewMode:
+ """Get the current application mode."""
+ return cls.__current
+
+ @classmethod
+ def set_current_mode(cls, mode: OCIOViewMode) -> None:
+ """Set the current application mode."""
+ if mode != cls.__current:
+ from .signal_router import SignalRouter
+
+ cls.__current = mode
+
+ signal_router = SignalRouter.get_instance()
+ signal_router.emit_mode_changed()
+
+
+OCIOViewMode.set_current_mode(OCIOViewMode.Edit)
diff --git a/src/apps/ocioview/ocioview/ref_space_manager.py b/src/apps/ocioview/ocioview/ref_space_manager.py
index 3de7ab84dc..022dc927df 100644
--- a/src/apps/ocioview/ocioview/ref_space_manager.py
+++ b/src/apps/ocioview/ocioview/ref_space_manager.py
@@ -78,7 +78,9 @@ def _update_scene_reference_space(cls) -> None:
or scene_ref_color_space.getTransform(
ocio.COLORSPACE_DIR_FROM_REFERENCE
)
- or scene_ref_color_space.getTransform(ocio.COLORSPACE_DIR_TO_REFERENCE)
+ or scene_ref_color_space.getTransform(
+ ocio.COLORSPACE_DIR_TO_REFERENCE
+ )
):
cls._ref_scene_name = None
@@ -89,8 +91,12 @@ def _update_scene_reference_space(cls) -> None:
):
if (
not color_space.isData()
- and not color_space.getTransform(ocio.COLORSPACE_DIR_FROM_REFERENCE)
- and not color_space.getTransform(ocio.COLORSPACE_DIR_TO_REFERENCE)
+ and not color_space.getTransform(
+ ocio.COLORSPACE_DIR_FROM_REFERENCE
+ )
+ and not color_space.getTransform(
+ ocio.COLORSPACE_DIR_TO_REFERENCE
+ )
):
cls._ref_scene_name = color_space.getName()
break
@@ -121,7 +127,9 @@ def _update_display_reference_space(cls) -> None:
# Verify existing display reference space
if cls._ref_display_name:
- display_ref_color_space = config.getColorSpace(cls._ref_display_name)
+ display_ref_color_space = config.getColorSpace(
+ cls._ref_display_name
+ )
if (
not display_ref_color_space
or display_ref_color_space.getReferenceSpaceType()
@@ -143,8 +151,12 @@ def _update_display_reference_space(cls) -> None:
):
if (
not color_space.isData()
- and not color_space.getTransform(ocio.COLORSPACE_DIR_FROM_REFERENCE)
- and not color_space.getTransform(ocio.COLORSPACE_DIR_TO_REFERENCE)
+ and not color_space.getTransform(
+ ocio.COLORSPACE_DIR_FROM_REFERENCE
+ )
+ and not color_space.getTransform(
+ ocio.COLORSPACE_DIR_TO_REFERENCE
+ )
):
cls._ref_display_name = color_space.getName()
break
diff --git a/src/apps/ocioview/ocioview/setup.py b/src/apps/ocioview/ocioview/setup.py
new file mode 100644
index 0000000000..33699bad6f
--- /dev/null
+++ b/src/apps/ocioview/ocioview/setup.py
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright Contributors to the OpenColorIO Project.
+
+import logging
+import os
+import sys
+from typing import Optional
+
+import PyOpenColorIO as ocio
+from PySide6 import QtCore, QtGui, QtWidgets
+
+from . import log_handlers # Import to initialize logging
+from .style import QSS, DarkPalette
+
+
+def excepthook(exc_type, exc_value, exc_tb):
+ """Log uncaught errors (especially those raised in Qt slots)."""
+ if issubclass(exc_type, KeyboardInterrupt):
+ sys.__excepthook__(exc_type, exc_value, exc_tb)
+ return
+ logging.error(f"{exc_value}", exc_info=exc_value)
+
+
+def setup_excepthook() -> None:
+ """Install exception hook."""
+ sys.excepthook = excepthook
+
+
+def setup_opengl() -> None:
+ """
+ OpenGL core profile is needed on macOS to access programmatic
+ pipeline.
+ """
+ gl_format = QtGui.QSurfaceFormat()
+ gl_format.setProfile(QtGui.QSurfaceFormat.CoreProfile)
+ gl_format.setSwapInterval(1)
+ gl_format.setVersion(4, 0)
+ QtGui.QSurfaceFormat.setDefaultFormat(gl_format)
+
+
+def setup_env() -> None:
+ """Clean OCIO environment to isolate working config."""
+ for env_var in (
+ ocio.OCIO_CONFIG_ENVVAR,
+ ocio.OCIO_ACTIVE_VIEWS_ENVVAR,
+ ocio.OCIO_ACTIVE_DISPLAYS_ENVVAR,
+ ocio.OCIO_INACTIVE_COLORSPACES_ENVVAR,
+ ocio.OCIO_OPTIMIZATION_FLAGS_ENVVAR,
+ ocio.OCIO_USER_CATEGORIES_ENVVAR,
+ ):
+ if env_var in os.environ:
+ del os.environ[env_var]
+
+
+def setup_style(app: QtWidgets.QApplication) -> None:
+ """Initialize app style."""
+ app.setStyle("fusion")
+ app.setPalette(DarkPalette())
+ app.setStyleSheet(QSS)
+ app.setEffectEnabled(QtCore.Qt.UI_AnimateCombo, False)
+
+ font = app.font()
+ font.setPointSize(8)
+ app.setFont(font)
+
+
+def setup_app(
+ app: Optional[QtWidgets.QApplication] = None,
+) -> QtWidgets.QApplication:
+ """Create and configure QApplication."""
+ # Setup environment
+ setup_excepthook()
+ setup_opengl()
+ setup_env()
+
+ # Create and/or setup app
+ if app is None:
+ app = QtWidgets.QApplication(sys.argv)
+ setup_style(app)
+
+ return app
diff --git a/src/apps/ocioview/ocioview/signal_router.py b/src/apps/ocioview/ocioview/signal_router.py
new file mode 100644
index 0000000000..7ed7834a17
--- /dev/null
+++ b/src/apps/ocioview/ocioview/signal_router.py
@@ -0,0 +1,85 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright Contributors to the OpenColorIO Project.
+
+from __future__ import annotations
+
+from typing import Optional
+
+from PySide6 import QtCore
+
+
+class SignalRouter(QtCore.QObject):
+ """
+ Singleton router which routes application-wide signals to
+ listeners.
+ """
+
+ mode_changed = QtCore.Signal()
+ """Emitted when the current application mode is changed."""
+
+ config_changed = QtCore.Signal()
+ """Emitted when the current config is modified."""
+
+ config_reloaded = QtCore.Signal()
+ """Emitted when the current config is reloaded or replaced."""
+
+ color_spaces_changed = QtCore.Signal()
+ """Emitted when a color space is added, removed, or changed."""
+
+ roles_changed = QtCore.Signal()
+ """Emitted when a color space role is added, removed, or changed."""
+
+ __instance: SignalRouter = None
+
+ @classmethod
+ def get_instance(cls) -> SignalRouter:
+ """Get singleton SignalRouter instance."""
+ if cls.__instance is None:
+ cls.__instance = SignalRouter()
+ return cls.__instance
+
+ def __init__(self, parent: Optional[QtCore.QObject] = None):
+ super().__init__(parent=parent)
+
+ # Only allow __init__ to be called once
+ if self.__instance is not None:
+ raise RuntimeError(
+ f"{self.__class__.__name__} is a singleton. Please call "
+ f"'get_instance' to access this type."
+ )
+ else:
+ self.__instance = self
+
+ def emit_mode_changed(self) -> None:
+ """
+ Notify listeners that the current application mode has changed.
+ """
+ self.mode_changed.emit()
+
+ def emit_config_changed(self) -> None:
+ """
+ Notify listeners that the current OCIO config has been modified.
+ """
+ self.config_changed.emit()
+
+ def emit_config_reloaded(self) -> None:
+ """
+ Notify listeners that the current OCIO config has been reloaded
+ or replaced and changed.
+ """
+ self.config_reloaded.emit()
+ self.emit_config_changed()
+
+ def emit_color_spaces_changed(self) -> None:
+ """
+ Notify listeners when a color space is added, removed, or
+ changed.
+ """
+ self.color_spaces_changed.emit()
+
+ def emit_roles_changed(self) -> None:
+ """
+ Notify listeners when a color space role is added, removed, or
+ changed.
+ """
+ self.roles_changed.emit()
diff --git a/src/apps/ocioview/ocioview/transform_manager.py b/src/apps/ocioview/ocioview/transform_manager.py
index c1990d2014..45950b048c 100644
--- a/src/apps/ocioview/ocioview/transform_manager.py
+++ b/src/apps/ocioview/ocioview/transform_manager.py
@@ -98,8 +98,12 @@ def set_subscription(
# Connect new subscription
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))
+ 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)
+ )
cls._tf_subscriptions[slot] = tf_subscription
# Inform menu subscribers of the menu change
@@ -110,7 +114,9 @@ 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_label))
+ 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:
@@ -182,7 +188,9 @@ def get_subscription_slot_icon(cls, slot: int) -> Union[QtGui.QIcon, None]:
}[slot]
color = cls.get_subscription_slot_color(slot)
return get_glyph_icon(
- f"ph.number-circle-{slot_word}", color=color, size=ICON_SIZE_ITEM
+ f"ph.number-circle-{slot_word}",
+ color=color,
+ size=ICON_SIZE_ITEM,
)
else:
return None
@@ -223,7 +231,9 @@ def subscribe_to_transform_menu(cls, menu_callback: Callable) -> None:
menu_callback(cls.get_subscription_menu_items())
@classmethod
- def subscribe_to_transform_subscription_init(cls, init_callback: Callable) -> None:
+ def subscribe_to_transform_subscription_init(
+ cls, init_callback: Callable
+ ) -> None:
"""
Subscribe to transform subscription initialization on all slots.
@@ -241,7 +251,9 @@ def subscribe_to_transform_subscription_init(cls, init_callback: Callable) -> No
break
@classmethod
- def subscribe_to_transforms_at(cls, slot: int, tf_callback: Callable) -> None:
+ def subscribe_to_transforms_at(
+ cls, slot: int, tf_callback: Callable
+ ) -> None:
"""
Subscribe to transform updates at the given slot number.
@@ -306,7 +318,8 @@ def _on_item_name_changed(cls, slot: int, item_label: str) -> None:
if tf_subscription is not None:
tf_subscription.item_label = item_label
cls._on_item_tf_changed(
- slot, *tf_subscription.item_model.get_item_transforms(item_label)
+ slot,
+ *tf_subscription.item_model.get_item_transforms(item_label),
)
cls._update_menu_items()
diff --git a/src/apps/ocioview/ocioview/transforms/cdl_edit.py b/src/apps/ocioview/ocioview/transforms/cdl_edit.py
index d411d16aea..5f404f187b 100644
--- a/src/apps/ocioview/ocioview/transforms/cdl_edit.py
+++ b/src/apps/ocioview/ocioview/transforms/cdl_edit.py
@@ -14,6 +14,7 @@
class CDLTransformEdit(BaseTransformEdit):
__icon_glyph__ = "ph.circles-three"
+ __tf_type_label__ = "CDL"
def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__(parent=parent)
diff --git a/src/apps/ocioview/ocioview/transforms/color_space_edit.py b/src/apps/ocioview/ocioview/transforms/color_space_edit.py
index db83ec88b5..e432f85865 100644
--- a/src/apps/ocioview/ocioview/transforms/color_space_edit.py
+++ b/src/apps/ocioview/ocioview/transforms/color_space_edit.py
@@ -6,8 +6,7 @@
import PyOpenColorIO as ocio
from PySide6 import QtCore
-from ..config_cache import ConfigCache
-from ..widgets import CheckBox, CallbackComboBox
+from ..widgets import CheckBox, ColorSpaceComboBox
from .transform_edit import BaseTransformEdit
from .transform_edit_factory import TransformEditFactory
@@ -19,11 +18,11 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__(parent=parent)
# Widget
- self.src_combo = CallbackComboBox(ConfigCache.get_color_space_names)
- self.src_combo.currentIndexChanged.connect(self._on_edit)
+ self.src_combo = ColorSpaceComboBox(include_roles=True)
+ self.src_combo.color_space_changed.connect(self._on_edit)
- self.dst_combo = CallbackComboBox(ConfigCache.get_color_space_names)
- self.dst_combo.currentIndexChanged.connect(self._on_edit)
+ self.dst_combo = ColorSpaceComboBox(include_roles=True)
+ self.dst_combo.color_space_changed.connect(self._on_edit)
self.data_bypass_check = CheckBox("Data Bypass")
self.data_bypass_check.stateChanged.connect(self._on_edit)
@@ -38,23 +37,25 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
def transform(self) -> ocio.ColorSpaceTransform:
transform = super().transform()
- transform.setSrc(self.src_combo.currentText())
- transform.setDst(self.dst_combo.currentText())
+ transform.setSrc(self.src_combo.color_space_name())
+ transform.setDst(self.dst_combo.color_space_name())
transform.setDataBypass(self.data_bypass_check.isChecked())
return transform
def update_from_transform(self, transform: ocio.Transform) -> None:
super().update_from_transform(transform)
- self.src_combo.setCurrentText(transform.getSrc())
- self.dst_combo.setCurrentText(transform.getDst())
+ self.src_combo.set_color_space(transform.getSrc())
+ self.dst_combo.set_color_space(transform.getDst())
self.data_bypass_check.setChecked(transform.getDataBypass())
def update_from_config(self):
"""
Update available color spaces from current config.
"""
- self.src_combo.update()
- self.dst_combo.update()
+ self.src_combo.update_color_spaces()
+ self.dst_combo.update_color_spaces()
-TransformEditFactory.register(ocio.ColorSpaceTransform, ColorSpaceTransformEdit)
+TransformEditFactory.register(
+ ocio.ColorSpaceTransform, ColorSpaceTransformEdit
+)
diff --git a/src/apps/ocioview/ocioview/transforms/display_view_edit.py b/src/apps/ocioview/ocioview/transforms/display_view_edit.py
index e5cb3bfaec..1894ffed2b 100644
--- a/src/apps/ocioview/ocioview/transforms/display_view_edit.py
+++ b/src/apps/ocioview/ocioview/transforms/display_view_edit.py
@@ -8,7 +8,7 @@
from ..config_cache import ConfigCache
from ..utils import SignalsBlocked
-from ..widgets import CheckBox, ComboBox, CallbackComboBox
+from ..widgets import CheckBox, ComboBox, CallbackComboBox, ColorSpaceComboBox
from .transform_edit import BaseTransformEdit
from .transform_edit_factory import TransformEditFactory
@@ -20,14 +20,18 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__(parent=parent)
# Widget
- self.src_combo = CallbackComboBox(ConfigCache.get_color_space_names)
- self.src_combo.currentIndexChanged.connect(self._on_edit)
+ self.src_combo = ColorSpaceComboBox(
+ ocio.SEARCH_REFERENCE_SPACE_SCENE, include_roles=True
+ )
+ self.src_combo.color_space_changed.connect(self._on_edit)
self.display_combo = CallbackComboBox(
ConfigCache.get_displays,
get_default_item=lambda: ocio.GetCurrentConfig().getDefaultDisplay(),
)
- self.display_combo.currentIndexChanged.connect(self._on_display_changed)
+ self.display_combo.currentIndexChanged.connect(
+ self._on_display_changed
+ )
self.display_combo.currentIndexChanged.connect(self._on_edit)
self.view_combo = ComboBox()
@@ -55,7 +59,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
def transform(self) -> ocio.ColorSpaceTransform:
transform = super().transform()
- transform.setSrc(self.src_combo.currentText())
+ transform.setSrc(self.src_combo.color_space_name())
transform.setDisplay(self.display_combo.currentText())
transform.setView(self.view_combo.currentText())
transform.setLooksBypass(self.looks_bypass_check.isChecked())
@@ -64,7 +68,7 @@ def transform(self) -> ocio.ColorSpaceTransform:
def update_from_transform(self, transform: ocio.Transform) -> None:
super().update_from_transform(transform)
- self.src_combo.setCurrentText(transform.getSrc())
+ self.src_combo.set_color_space(transform.getSrc())
self.display_combo.setCurrentText(transform.getDisplay())
self.view_combo.setCurrentText(transform.getView())
self.looks_bypass_check.setChecked(transform.getLooksBypass())
@@ -75,8 +79,8 @@ def update_from_config(self):
Update available color spaces and displays from the current
config.
"""
- self.src_combo.update()
- self.display_combo.update()
+ self.src_combo.update_color_spaces()
+ self.display_combo.update_items()
self._on_display_changed(self.display_combo.currentIndex())
@QtCore.Slot(int)
@@ -88,11 +92,13 @@ def _on_display_changed(self, index: int):
config = ocio.GetCurrentConfig()
display = self.display_combo.itemText(index)
view = self.view_combo.currentText()
- color_space_name = self.src_combo.currentText()
+ color_space_name = self.src_combo.color_space_name()
with SignalsBlocked(self.view_combo):
self.view_combo.clear()
- self.view_combo.addItems(ConfigCache.get_views(display, color_space_name))
+ self.view_combo.addItems(
+ ConfigCache.get_views(display, color_space_name)
+ )
view_index = self.view_combo.findText(view)
if view_index != -1:
@@ -103,4 +109,6 @@ def _on_display_changed(self, index: int):
)
-TransformEditFactory.register(ocio.DisplayViewTransform, DisplayViewTransformEdit)
+TransformEditFactory.register(
+ ocio.DisplayViewTransform, DisplayViewTransformEdit
+)
diff --git a/src/apps/ocioview/ocioview/transforms/look_edit.py b/src/apps/ocioview/ocioview/transforms/look_edit.py
index e6b0005a1e..086a203083 100644
--- a/src/apps/ocioview/ocioview/transforms/look_edit.py
+++ b/src/apps/ocioview/ocioview/transforms/look_edit.py
@@ -6,8 +6,7 @@
import PyOpenColorIO as ocio
from PySide6 import QtCore
-from ..config_cache import ConfigCache
-from ..widgets import CheckBox, CallbackComboBox, LineEdit
+from ..widgets import CheckBox, ColorSpaceComboBox, LineEdit
from .transform_edit import BaseTransformEdit
from .transform_edit_factory import TransformEditFactory
@@ -19,14 +18,18 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
super().__init__(parent=parent)
# Widgets
- self.src_combo = CallbackComboBox(ConfigCache.get_color_space_names)
- self.src_combo.currentIndexChanged.connect(self._on_edit)
+ self.src_combo = ColorSpaceComboBox(include_roles=True)
+ self.src_combo.color_space_changed.connect(self._on_edit)
- self.dst_combo = CallbackComboBox(ConfigCache.get_color_space_names)
- self.dst_combo.currentIndexChanged.connect(self._on_edit)
+ self.dst_combo = ColorSpaceComboBox(include_roles=True)
+ self.dst_combo.color_space_changed.connect(self._on_edit)
- self.skip_color_space_conversion_check = CheckBox("Skip Color Space Conversion")
- self.skip_color_space_conversion_check.stateChanged.connect(self._on_edit)
+ self.skip_color_space_conversion_check = CheckBox(
+ "Skip Color Space Conversion"
+ )
+ self.skip_color_space_conversion_check.stateChanged.connect(
+ self._on_edit
+ )
# TODO: Add look completer and validator
self.looks_edit = LineEdit()
@@ -43,8 +46,8 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
def transform(self) -> ocio.ColorSpaceTransform:
transform = super().transform()
- transform.setSrc(self.src_combo.currentText())
- transform.setDst(self.dst_combo.currentText())
+ transform.setSrc(self.src_combo.color_space_name())
+ transform.setDst(self.dst_combo.color_space_name())
transform.setLooks(self.looks_edit.text())
transform.setSkipColorSpaceConversion(
self.skip_color_space_conversion_check.isChecked()
@@ -53,8 +56,8 @@ def transform(self) -> ocio.ColorSpaceTransform:
def update_from_transform(self, transform: ocio.Transform) -> None:
super().update_from_transform(transform)
- self.src_combo.setCurrentText(transform.getSrc())
- self.dst_combo.setCurrentText(transform.getDst())
+ self.src_combo.set_color_space(transform.getSrc())
+ self.dst_combo.set_color_space(transform.getDst())
self.looks_edit.setText(transform.getLooks())
self.skip_color_space_conversion_check.setChecked(
transform.getSkipColorSpaceConversion()
@@ -64,8 +67,8 @@ def update_from_config(self):
"""
Update available color spaces from current config.
"""
- self.src_combo.update()
- self.dst_combo.update()
+ self.src_combo.update_color_spaces()
+ self.dst_combo.update_color_spaces()
TransformEditFactory.register(ocio.LookTransform, LookTransformEdit)
diff --git a/src/apps/ocioview/ocioview/transforms/transform_edit.py b/src/apps/ocioview/ocioview/transforms/transform_edit.py
index c517a71d86..99a3cc021c 100644
--- a/src/apps/ocioview/ocioview/transforms/transform_edit.py
+++ b/src/apps/ocioview/ocioview/transforms/transform_edit.py
@@ -10,7 +10,10 @@
from PySide6 import QtCore, QtGui, QtWidgets
from ..constants import ICON_SIZE_ITEM, BORDER_COLOR_ROLE
-from ..style import apply_top_tool_bar_style, apply_widget_with_top_tool_bar_style
+from ..style import (
+ apply_top_tool_bar_style,
+ apply_widget_with_top_tool_bar_style,
+)
from ..utils import get_glyph_icon, item_type_label
from ..widgets import EnumComboBox, FormLayout
@@ -45,7 +48,9 @@ def transform_type_icon(cls) -> QtGui.QIcon:
:return: Transform type icon
"""
if cls.__icon__ is None:
- cls.__icon__ = get_glyph_icon(cls.__icon_glyph__, size=ICON_SIZE_ITEM)
+ cls.__icon__ = get_glyph_icon(
+ cls.__icon_glyph__, size=ICON_SIZE_ITEM
+ )
return cls.__icon__
@classmethod
@@ -55,7 +60,9 @@ def transform_type_label(cls) -> str:
"""
if cls.__tf_type_label__ is None:
# Remove trailing "Transform" token
- cls.__tf_type_label__ = item_type_label(cls.__tf_type__).rsplit(" ", 1)[0]
+ cls.__tf_type_label__ = item_type_label(cls.__tf_type__).rsplit(
+ " ", 1
+ )[0]
return cls.__tf_type_label__
@classmethod
@@ -96,7 +103,9 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.icon_label = None
if self.__icon_glyph__ is not None:
- self.icon_label = get_glyph_icon(self.__icon_glyph__, as_widget=True)
+ self.icon_label = get_glyph_icon(
+ self.__icon_glyph__, as_widget=True
+ )
self.expand_button = QtWidgets.QToolButton()
self.expand_button.setIcon(self._collapse_icon)
@@ -111,7 +120,9 @@ def __init__(self, parent: Optional[QtCore.QObject] = None):
self.move_down_button = QtWidgets.QToolButton()
self.move_down_button.setIcon(get_glyph_icon("ph.arrow-down"))
self.move_down_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
- self.move_down_button.released.connect(partial(self.moved_down.emit, self))
+ self.move_down_button.released.connect(
+ partial(self.moved_down.emit, self)
+ )
self.delete_button = QtWidgets.QToolButton()
self.delete_button.setIcon(get_glyph_icon("ph.x"))
@@ -238,7 +249,11 @@ def _update_state(self) -> None:
"""
if self._tf_frame.isHidden():
self.expand_button.setIcon(self._expand_icon)
- apply_top_tool_bar_style(self._header_frame, border_bottom_radius=3)
+ apply_top_tool_bar_style(
+ self._header_frame, border_bottom_radius=3
+ )
else:
self.expand_button.setIcon(self._collapse_icon)
- apply_top_tool_bar_style(self._header_frame, border_bottom_radius=0)
+ apply_top_tool_bar_style(
+ self._header_frame, border_bottom_radius=0
+ )
diff --git a/src/apps/ocioview/ocioview/utils.py b/src/apps/ocioview/ocioview/utils.py
index 32b46ee925..d1b1c27042 100644
--- a/src/apps/ocioview/ocioview/utils.py
+++ b/src/apps/ocioview/ocioview/utils.py
@@ -138,7 +138,9 @@ def item_type_label(item_type: type) -> str:
:param item_type: Config item type
:return: Friendly type name
"""
- return " ".join(filter(None, re.split(r"([A-Z]+[a-z]+)", item_type.__name__)))
+ return " ".join(
+ filter(None, re.split(r"([A-Z]+[a-z]+)", item_type.__name__))
+ )
def m44_to_m33(m44: list) -> list:
@@ -181,7 +183,9 @@ def config_to_html(config: ocio.Config) -> str:
)
-def processor_to_ctf_html(processor: ocio.Processor) -> tuple[str, ocio.GroupTransform]:
+def processor_to_ctf_html(
+ processor: ocio.Processor,
+) -> tuple[str, ocio.GroupTransform]:
"""Return processor CTF formatted as HTML."""
config = ocio.GetCurrentConfig()
group_tf = processor.createGroupTransform()
@@ -223,14 +227,20 @@ def processor_to_shader_html(
Return processor shader in the requested language, formatted as
HTML.
"""
- gpu_shader_desc = ocio.GpuShaderDesc.CreateShaderDesc(language=gpu_language)
+ gpu_shader_desc = ocio.GpuShaderDesc.CreateShaderDesc(
+ language=gpu_language
+ )
gpu_proc.extractGpuShaderInfo(gpu_shader_desc)
shader_data = gpu_shader_desc.getShaderText()
return increase_html_lineno_padding(
highlight(
shader_data,
- (GLShaderLexer if "GLSL" in gpu_language.name else HLSLShaderLexer)(),
+ (
+ GLShaderLexer
+ if "GLSL" in gpu_language.name
+ else HLSLShaderLexer
+ )(),
HtmlFormatter(linenos="inline"),
)
)
@@ -290,7 +300,7 @@ def color_space_to_rgb_colourspace(color_space: str) -> RGB_Colourspace | None:
config = ocio.GetCurrentConfig()
if (not config.hasRole(ocio.ROLE_INTERCHANGE_DISPLAY)) or (
- color_space not in ConfigCache.get_color_space_names()
+ color_space not in ConfigCache.get_color_space_names()
):
return None
diff --git a/src/apps/ocioview/ocioview/viewer/image_plane.py b/src/apps/ocioview/ocioview/viewer/image_plane.py
index aa128bf0f5..5117934ada 100644
--- a/src/apps/ocioview/ocioview/viewer/image_plane.py
+++ b/src/apps/ocioview/ocioview/viewer/image_plane.py
@@ -97,7 +97,9 @@ class ImagePlane(QtOpenGLWidgets.QOpenGLWidget):
"""
image_loaded = QtCore.Signal(Path, int, int)
- sample_changed = QtCore.Signal(int, int, float, float, float, float, float, float)
+ sample_changed = QtCore.Signal(
+ int, int, float, float, float, float, float, float
+ )
scale_changed = QtCore.Signal(float)
tf_subscription_requested = QtCore.Signal(int)
@@ -184,8 +186,12 @@ def initializeGL(self) -> None:
ctypes.c_void_p(0),
)
- GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE)
- GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE)
+ GL.glTexParameteri(
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE
+ )
+ GL.glTexParameteri(
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE
+ )
self._set_ocio_tex_params(GL.GL_TEXTURE_2D, ocio.INTERP_LINEAR)
# Init image plane
@@ -244,7 +250,9 @@ def initializeGL(self) -> None:
plane_position_data,
GL.GL_STATIC_DRAW,
)
- GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0))
+ GL.glVertexAttribPointer(
+ 0, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0)
+ )
GL.glEnableVertexAttribArray(0)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._plane_tex_coord_vbo)
@@ -254,7 +262,9 @@ def initializeGL(self) -> None:
plane_tex_coord_data,
GL.GL_STATIC_DRAW,
)
- GL.glVertexAttribPointer(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0))
+ GL.glVertexAttribPointer(
+ 1, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0)
+ )
GL.glEnableVertexAttribArray(1)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._plane_index_vbo)
@@ -310,10 +320,14 @@ def paintGL(self) -> None:
# Set uniforms
mvp_mat = self._proj_mat @ self._model_view_mat
- mvp_mat_loc = GL.glGetUniformLocation(self._shader_program, "mvpMat")
+ mvp_mat_loc = GL.glGetUniformLocation(
+ self._shader_program, "mvpMat"
+ )
GL.glUniformMatrix4fv(mvp_mat_loc, 1, GL.GL_FALSE, mvp_mat.T)
- image_tex_loc = GL.glGetUniformLocation(self._shader_program, "imageTex")
+ image_tex_loc = GL.glGetUniformLocation(
+ self._shader_program, "imageTex"
+ )
GL.glUniform1i(image_tex_loc, 0)
# Bind texture, VAO, and draw
@@ -425,7 +439,8 @@ def clear_transform(self) -> None:
self.update_ocio_proc(
ProcessorContext(self._ocio_proc_context.input_color_space),
- force_update=True)
+ force_update=True,
+ )
def reset_ocio_proc(self, update: bool = False) -> None:
"""
@@ -448,7 +463,7 @@ def update_ocio_proc(
transform: Optional[ocio.Transform] = None,
channel: Optional[int] = None,
force_update: bool = False,
- ):
+ ) -> None:
"""
Update one or more aspects of the OCIO GPU renderer. Parameters
are cached, so not providing a parameter maintains the existing
@@ -474,7 +489,9 @@ def update_ocio_proc(
config = ocio.GetCurrentConfig()
has_scene_linear = config.hasRole(ocio.ROLE_SCENE_LINEAR)
- scene_ref_name = ReferenceSpaceManager.scene_reference_space().getName()
+ scene_ref_name = (
+ ReferenceSpaceManager.scene_reference_space().getName()
+ )
# Build simplified viewing pipeline:
# - GPU: For viewport rendering
@@ -483,7 +500,11 @@ 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_proc_context:
+ if (
+ has_scene_linear
+ and self._ocio_proc_context
+ and self._ocio_proc_context.input_color_space
+ ):
to_scene_linear = ocio.ColorSpaceTransform(
src=self._ocio_proc_context.input_color_space,
dst=ocio.ROLE_SCENE_LINEAR,
@@ -501,7 +522,10 @@ 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 self._ocio_proc_context and self._ocio_proc_context.input_color_space:
+ 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
@@ -510,7 +534,8 @@ def update_ocio_proc(
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
+ 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)
@@ -521,9 +546,13 @@ def update_ocio_proc(
cpu_viewing_pipeline.appendTransform(self._ocio_tf)
# Or restore input color space, if known
- elif self._ocio_proc_context and self._ocio_proc_context.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_proc_context.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)
@@ -544,7 +573,14 @@ def update_ocio_proc(
)
# Create GPU processor
- gpu_proc = config.getProcessor(gpu_viewing_pipeline, ocio.TRANSFORM_DIR_FORWARD)
+ try:
+ gpu_proc = config.getProcessor(
+ gpu_viewing_pipeline, ocio.TRANSFORM_DIR_FORWARD
+ )
+ except ocio.Exception:
+ # Config may have changed between transform creation and now. If this
+ # doesn't error, CPU processor construction should succeed.
+ return
if gpu_proc.getCacheID() != self._ocio_proc_cache_id:
# Update CPU processor
@@ -569,12 +605,16 @@ def update_ocio_proc(
self._update_ocio_dyn_prop(
ocio.DYNAMIC_PROPERTY_EXPOSURE, self._ocio_exposure
)
- self._update_ocio_dyn_prop(ocio.DYNAMIC_PROPERTY_GAMMA, self._ocio_gamma)
+ self._update_ocio_dyn_prop(
+ ocio.DYNAMIC_PROPERTY_GAMMA, self._ocio_gamma
+ )
self.update()
# Log processor change after render
- message_queue.put_nowait((self._ocio_proc_context, self._ocio_proc))
+ message_queue.put_nowait(
+ (self._ocio_proc_context, self._ocio_proc)
+ )
elif force_update:
self.update()
@@ -582,8 +622,13 @@ def update_ocio_proc(
# The transform and processor has not changed, but other app components
# which view it may have dropped the reference. Log processor to update
# them as needed.
- 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))
+ 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,10 +708,12 @@ def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
]
)
model_pos = (
- np.linalg.inv(self._proj_mat @ self._model_view_mat) @ screen_pos
+ np.linalg.inv(self._proj_mat @ self._model_view_mat)
+ @ screen_pos
)
pixel_pos = (
- np.array([model_pos[0] + 0.5, model_pos[1] + 0.5]) * self._image_size
+ np.array([model_pos[0] + 0.5, model_pos[1] + 0.5])
+ * self._image_size
)
# Broadcast sample position
@@ -689,7 +736,9 @@ def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
else:
pixel_output = pixel_input.copy()
- self.sample_changed.emit(pixel_x, pixel_y, *pixel_input, *pixel_output)
+ self.sample_changed.emit(
+ pixel_x, pixel_y, *pixel_input, *pixel_output
+ )
else:
# Out of image bounds
self.sample_changed.emit(-1, -1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
@@ -799,7 +848,9 @@ def _install_shortcuts(self) -> None:
# Number keys = Subscribe to transform @ slot
for i in range(10):
- subscribe_shortcut = QtGui.QShortcut(QtGui.QKeySequence(str(i)), self)
+ subscribe_shortcut = QtGui.QShortcut(
+ QtGui.QKeySequence(str(i)), self
+ )
subscribe_shortcut.activated.connect(
lambda slot=i: self.tf_subscription_requested.emit(slot)
)
@@ -807,7 +858,9 @@ def _install_shortcuts(self) -> None:
# Ctrl + Number keys = Power of 2 scale: 1 = x1, 2 = x2, 3 = x4, ...
for i in range(9):
- scale_shortcut = QtGui.QShortcut(QtGui.QKeySequence(f"Ctrl+{i + 1}"), self)
+ scale_shortcut = QtGui.QShortcut(
+ QtGui.QKeySequence(f"Ctrl+{i + 1}"), self
+ )
scale_shortcut.activated.connect(
lambda exponent=i: self.zoom(
self.rect().center(), float(2**exponent), absolute=True
@@ -840,7 +893,9 @@ def _compile_shader(
compile_status = GL.glGetShaderiv(shader, GL.GL_COMPILE_STATUS)
if not compile_status:
compile_log = GL.glGetShaderInfoLog(shader)
- logger.error("Shader program compile error: {log}".format(log=compile_log))
+ logger.error(
+ "Shader program compile error: {log}".format(log=compile_log)
+ )
return None
return shader
@@ -872,7 +927,9 @@ def _build_program(self, force: bool = False) -> None:
# Vert shader only needs to be built once
if not self._vert_shader:
- self._vert_shader = self._compile_shader(GLSL_VERT_SRC, GL.GL_VERTEX_SHADER)
+ self._vert_shader = self._compile_shader(
+ GLSL_VERT_SRC, GL.GL_VERTEX_SHADER
+ )
if not self._vert_shader:
return
@@ -889,7 +946,9 @@ def _build_program(self, force: bool = False) -> None:
frag_src = GLSL_FRAG_OCIO_SRC_FMT.format(
ocio_src=self._ocio_shader_desc.getShaderText()
)
- self._frag_shader = self._compile_shader(frag_src, GL.GL_FRAGMENT_SHADER)
+ self._frag_shader = self._compile_shader(
+ frag_src, GL.GL_FRAGMENT_SHADER
+ )
if not self._frag_shader:
return
@@ -900,10 +959,14 @@ def _build_program(self, force: bool = False) -> None:
GL.glBindAttribLocation(self._shader_program, 1, "in_texCoord")
GL.glLinkProgram(self._shader_program)
- link_status = GL.glGetProgramiv(self._shader_program, GL.GL_LINK_STATUS)
+ link_status = GL.glGetProgramiv(
+ self._shader_program, GL.GL_LINK_STATUS
+ )
if not link_status:
link_log = GL.glGetProgramInfoLog(self._shader_program)
- logger.error("Shader program link error: {log}".format(log=link_log))
+ logger.error(
+ "Shader program link error: {log}".format(log=link_log)
+ )
return
# Store cache ID to detect reuse
@@ -939,7 +1002,9 @@ def _orthographic_proj_matrix(
b = 2 / top_minus_bottom
c = -2 / far_minus_near
- return np.array([[a, 0, 0, tx], [0, b, 0, ty], [0, 0, c, tz], [0, 0, 0, 1]])
+ return np.array(
+ [[a, 0, 0, tx], [0, b, 0, ty], [0, 0, c, tz], [0, 0, 0, 1]]
+ )
def _update_model_view_mat(self, update: bool = True) -> None:
"""
@@ -953,7 +1018,12 @@ def _update_model_view_mat(self, update: bool = True) -> None:
# Flip Y to account for different OIIO/OpenGL image origin
self._model_view_mat *= [1.0, -1.0, 1.0, 1.0]
- self._model_view_mat *= [self._image_scale, self._image_scale, 1.0, 1.0]
+ self._model_view_mat *= [
+ self._image_scale,
+ self._image_scale,
+ 1.0,
+ 1.0,
+ ]
self._model_view_mat[:2, -1] += [
self._image_pos[0] * self._image_scale,
-self._image_pos[1] * self._image_scale,
@@ -983,11 +1053,19 @@ def _set_ocio_tex_params(
self.makeCurrent()
if interpolation == ocio.INTERP_NEAREST:
- GL.glTexParameteri(tex_type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
- GL.glTexParameteri(tex_type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
+ GL.glTexParameteri(
+ tex_type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST
+ )
+ GL.glTexParameteri(
+ tex_type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST
+ )
else:
- GL.glTexParameteri(tex_type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
- GL.glTexParameteri(tex_type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
+ GL.glTexParameteri(
+ tex_type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR
+ )
+ GL.glTexParameteri(
+ tex_type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR
+ )
def _allocate_ocio_tex(self) -> None:
"""
@@ -1088,7 +1166,13 @@ def _allocate_ocio_tex(self) -> None:
)
self._ocio_tex_ids.append(
- (tex, tex_info.textureName, tex_info.samplerName, tex_type, tex_index)
+ (
+ tex,
+ tex_info.textureName,
+ tex_info.samplerName,
+ tex_type,
+ tex_index,
+ )
)
tex_index += 1
@@ -1098,7 +1182,13 @@ def _del_ocio_tex(self) -> None:
"""
self.makeCurrent()
- for tex, tex_name, sampler_name, tex_type, tex_index in self._ocio_tex_ids:
+ for (
+ tex,
+ tex_name,
+ sampler_name,
+ tex_type,
+ tex_index,
+ ) in self._ocio_tex_ids:
GL.glDeleteTextures([tex])
del self._ocio_tex_ids[:]
@@ -1108,11 +1198,18 @@ def _use_ocio_tex(self) -> None:
"""
self.makeCurrent()
- for tex, tex_name, sampler_name, tex_type, tex_index in self._ocio_tex_ids:
+ for (
+ tex,
+ tex_name,
+ sampler_name,
+ tex_type,
+ tex_index,
+ ) in self._ocio_tex_ids:
GL.glActiveTexture(GL.GL_TEXTURE0 + tex_index)
GL.glBindTexture(tex_type, tex)
GL.glUniform1i(
- GL.glGetUniformLocation(self._shader_program, sampler_name), tex_index
+ GL.glGetUniformLocation(self._shader_program, sampler_name),
+ tex_index,
)
def _del_ocio_uniforms(self) -> None:
diff --git a/src/apps/ocioview/ocioview/viewer/image_viewer.py b/src/apps/ocioview/ocioview/viewer/image_viewer.py
index ca0fde2198..3a8be5eeb1 100644
--- a/src/apps/ocioview/ocioview/viewer/image_viewer.py
+++ b/src/apps/ocioview/ocioview/viewer/image_viewer.py
@@ -5,17 +5,28 @@
from contextlib import contextmanager
from pathlib import Path
-from typing import Generator, Optional, Type
+from typing import Generator, Optional, Union
import PyOpenColorIO as ocio
from PySide6 import QtCore, QtGui, QtWidgets
+from ..config_cache import ConfigCache
+from ..constants import (
+ GRAY_COLOR,
+ R_COLOR,
+ G_COLOR,
+ B_COLOR,
+ ICON_SIZE_TAB,
+)
+from ..items.display_model import DisplayModel
+from ..items.view_model import ViewModel
+from ..mode import OCIOViewMode
from ..processor_context import ProcessorContext
+from ..ref_space_manager import ReferenceSpaceManager
+from ..signal_router import SignalRouter
from ..transform_manager import TransformManager
-from ..config_cache import ConfigCache
-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 ..widgets import ComboBox, CallbackComboBox, ColorSpaceComboBox
from .image_plane import ImagePlane
@@ -30,7 +41,7 @@ class ViewerChannels(object):
class ImageViewer(QtWidgets.QWidget):
"""
- Main image viewer widget, which can display an image with internal
+ Image viewer widget, which can display an image with internal
32-bit float precision.
"""
@@ -44,65 +55,95 @@ class ImageViewer(QtWidgets.QWidget):
PASSTHROUGH = "passthrough"
PASSTHROUGH_LABEL = FMT_GRAY_LABEL.format(v=f"{PASSTHROUGH}:")
- 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
- """
+ """Get viewer type icon."""
return get_glyph_icon("mdi6.image-outline", size=ICON_SIZE_TAB)
@classmethod
def viewer_type_label(cls) -> str:
- """
- :return: Friendly viewer type name
- """
+ """Get friendly viewer type name."""
return "Image Viewer"
def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(parent)
+ self._sample_format = ""
self._tf_subscription_slot = -1
self._tf_fwd = None
self._tf_inv = None
- self._sample_format = ""
# Widgets
+ # ---------------------------------------------------------------------
+
+ # Viewport
self.image_plane = ImagePlane(self)
self.image_plane.setSizePolicy(
QtWidgets.QSizePolicy(
- QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding,
)
)
- self.input_color_space_label = get_glyph_icon("mdi6.import", as_widget=True)
+ # Input color space
+ self.input_color_space_label = get_glyph_icon(
+ "mdi6.import", as_widget=True
+ )
self.input_color_space_label.setToolTip("Input color space")
- self.input_color_space_box = CallbackComboBox(
- lambda: ConfigCache.get_color_space_names(ocio.SEARCH_REFERENCE_SPACE_SCENE)
+ self.input_color_space_box = ColorSpaceComboBox(include_roles=True)
+ self.input_color_space_box.setToolTip(
+ self.input_color_space_label.toolTip()
)
- self.input_color_space_box.setFixedHeight(self.WIDGET_HEIGHT_IO)
- self.input_color_space_box.setToolTip(self.input_color_space_label.toolTip())
+ # Edit mode
self.tf_label = get_glyph_icon("mdi6.export", as_widget=True)
self.tf_box = ComboBox()
- self.tf_box.setFixedHeight(self.WIDGET_HEIGHT_IO)
self.tf_box.setToolTip("Output transform")
self._tf_direction_forward_icon = get_glyph_icon("mdi6.layers-plus")
self._tf_direction_inverse_icon = get_glyph_icon("mdi6.layers-minus")
self.tf_direction_button = QtWidgets.QPushButton()
- self.tf_direction_button.setFixedHeight(self.WIDGET_HEIGHT_IO)
self.tf_direction_button.setCheckable(True)
self.tf_direction_button.setChecked(False)
self.tf_direction_button.setIcon(self._tf_direction_forward_icon)
self.tf_direction_button.setToolTip("Transform direction: Forward")
+ self.output_tf_direction_label = QtWidgets.QLabel("+")
+
+ # Preview mode
+ self.display_view_label = get_glyph_icon(
+ ViewModel.__icon_glyph__, as_widget=True
+ )
+ self.display_view_label.setToolTip(
+ f"{DisplayModel.item_type_label()} / {ViewModel.item_type_label()}"
+ )
+
+ self.display_box = CallbackComboBox(
+ self._get_displays,
+ get_default_item=self._get_default_display,
+ )
+ self.display_box.setSizeAdjustPolicy(
+ QtWidgets.QComboBox.AdjustToContents
+ )
+ self.display_box.setToolTip(DisplayModel.item_type_label())
+ self.display_box.currentIndexChanged[int].connect(
+ self._on_display_changed
+ )
+
+ self.view_box = CallbackComboBox(
+ self._get_views,
+ get_default_item=self._get_default_view,
+ )
+ self.view_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
+ self.view_box.setToolTip(ViewModel.item_type_label())
+ self.view_box.currentIndexChanged[int].connect(self._on_view_changed)
+
+ # Image adjustments
self.exposure_label = get_glyph_icon("ph.aperture", as_widget=True)
self.exposure_label.setToolTip("Exposure")
self.exposure_box = QtWidgets.QDoubleSpinBox()
@@ -125,19 +166,32 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
"Sample precision (number of digits after the decimal point)"
)
self.sample_precision_box = QtWidgets.QSpinBox()
- self.sample_precision_box.setToolTip(self.sample_precision_label.toolTip())
+ self.sample_precision_box.setToolTip(
+ self.sample_precision_label.toolTip()
+ )
self.sample_precision_box.setValue(5)
+ # Info and inspect labels
self.image_name_label = QtWidgets.QLabel()
- self.image_scale_label = QtWidgets.QLabel(self.FMT_IMAGE_SCALE.format(s=100))
+ self.image_scale_label = QtWidgets.QLabel(
+ self.FMT_IMAGE_SCALE.format(s=100)
+ )
- self.input_w_label = QtWidgets.QLabel(self.FMT_GRAY_LABEL.format(v="W:"))
+ self.input_w_label = QtWidgets.QLabel(
+ self.FMT_GRAY_LABEL.format(v="W:")
+ )
self.image_w_value_label = QtWidgets.QLabel("0")
- self.input_h_label = QtWidgets.QLabel(self.FMT_GRAY_LABEL.format(v="H:"))
+ self.input_h_label = QtWidgets.QLabel(
+ self.FMT_GRAY_LABEL.format(v="H:")
+ )
self.image_h_value_label = QtWidgets.QLabel("0")
- self.input_x_label = QtWidgets.QLabel(self.FMT_GRAY_LABEL.format(v="X:"))
+ self.input_x_label = QtWidgets.QLabel(
+ self.FMT_GRAY_LABEL.format(v="X:")
+ )
self.image_x_value_label = QtWidgets.QLabel("0")
- self.input_y_label = QtWidgets.QLabel(self.FMT_GRAY_LABEL.format(v="Y:"))
+ self.input_y_label = QtWidgets.QLabel(
+ self.FMT_GRAY_LABEL.format(v="Y:")
+ )
self.image_y_value_label = QtWidgets.QLabel("0")
self.input_sample_label = get_glyph_icon(
@@ -152,7 +206,6 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
self.FMT_SWATCH_CSS.format(r=0, g=0, b=0)
)
- self.output_tf_direction_label = QtWidgets.QLabel("+")
self.output_sample_label = get_glyph_icon(
"mdi6.export", color=GRAY_COLOR, as_widget=True
)
@@ -166,137 +219,234 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
)
# Layout
- info_layout = QtWidgets.QHBoxLayout()
- info_layout.setContentsMargins(8, 8, 8, 8)
- info_layout.addWidget(self.image_name_label)
- info_layout.addStretch()
- info_layout.addWidget(self.image_scale_label)
+ # ---------------------------------------------------------------------
+
+ # Info and inspect labels
+ self.info_layout = QtWidgets.QHBoxLayout()
+ self.info_layout.setContentsMargins(8, 8, 8, 8)
+ self.info_layout.addWidget(self.image_name_label)
+ self.info_layout.addStretch()
+ self.info_layout.addWidget(self.image_scale_label)
self.info_bar = QtWidgets.QFrame()
- self.info_bar.setObjectName("image_viewer__info_bar")
+ self.info_bar.setObjectName("base_image_viewer__info_bar")
self.info_bar.setStyleSheet(
- "QFrame#image_viewer__info_bar { background-color: black; }"
- )
- self.info_bar.setLayout(info_layout)
-
- inspect_layout = QtWidgets.QGridLayout()
- inspect_layout.setContentsMargins(8, 8, 8, 8)
-
- inspect_layout.addWidget(self.input_w_label, 0, 0, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.image_w_value_label, 0, 1, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_h_label, 0, 2, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.image_h_value_label, 0, 3, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(QtWidgets.QLabel(), 0, 4)
- inspect_layout.addWidget(self.input_sample_label, 0, 6, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_r_sample_label, 0, 7, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_g_sample_label, 0, 8, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_b_sample_label, 0, 9, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_sample_swatch, 0, 10, QtCore.Qt.AlignLeft)
-
- inspect_layout.addWidget(self.input_x_label, 1, 0, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.image_x_value_label, 1, 1, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.input_y_label, 1, 2, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.image_y_value_label, 1, 3, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(QtWidgets.QLabel(), 1, 4)
- inspect_layout.setColumnStretch(4, 1)
- inspect_layout.addWidget(
- self.output_tf_direction_label, 1, 5, QtCore.Qt.AlignRight
+ "QFrame#base_image_viewer__info_bar { background-color: black; }"
+ )
+ self.info_bar.setLayout(self.info_layout)
+
+ self.inspect_layout = QtWidgets.QGridLayout()
+ self.inspect_layout.setContentsMargins(8, 8, 8, 8)
+
+ self.inspect_layout.addWidget(
+ self.input_w_label, 0, 0, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.image_w_value_label, 0, 1, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_h_label, 0, 2, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.image_h_value_label, 0, 3, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(QtWidgets.QLabel(), 0, 4)
+ self.inspect_layout.addWidget(
+ self.input_sample_label, 0, 6, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_r_sample_label, 0, 7, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_g_sample_label, 0, 8, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_b_sample_label, 0, 9, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_sample_swatch, 0, 10, QtCore.Qt.AlignLeft
+ )
+
+ self.inspect_layout.addWidget(
+ self.input_x_label, 1, 0, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.image_x_value_label, 1, 1, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.input_y_label, 1, 2, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.image_y_value_label, 1, 3, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(QtWidgets.QLabel(), 1, 4)
+ self.inspect_layout.setColumnStretch(4, 1)
+ self.inspect_layout.addWidget(
+ self.output_sample_label, 1, 6, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.output_r_sample_label, 1, 7, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.output_g_sample_label, 1, 8, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.output_b_sample_label, 1, 9, QtCore.Qt.AlignRight
+ )
+ self.inspect_layout.addWidget(
+ self.output_sample_swatch, 1, 10, QtCore.Qt.AlignLeft
)
- inspect_layout.addWidget(self.output_sample_label, 1, 6, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.output_r_sample_label, 1, 7, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.output_g_sample_label, 1, 8, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.output_b_sample_label, 1, 9, QtCore.Qt.AlignRight)
- inspect_layout.addWidget(self.output_sample_swatch, 1, 10, QtCore.Qt.AlignLeft)
self.inspect_bar = QtWidgets.QFrame()
- self.inspect_bar.setObjectName("image_viewer__status_bar")
+ self.inspect_bar.setObjectName("base_image_viewer__status_bar")
self.inspect_bar.setStyleSheet(
- "QFrame#image_viewer__status_bar { background-color: black; }"
- )
- self.inspect_bar.setLayout(inspect_layout)
-
- tf_layout = QtWidgets.QHBoxLayout()
- tf_layout.setContentsMargins(0, 0, 0, 0)
- tf_layout.setSpacing(0)
- tf_layout.addWidget(self.tf_box)
- tf_layout.setStretch(0, 1)
- tf_layout.addWidget(self.tf_direction_button)
-
- io_layout = QtWidgets.QHBoxLayout()
- io_layout.addWidget(self.input_color_space_label)
- io_layout.addWidget(self.input_color_space_box)
- io_layout.setStretch(1, 1)
- io_layout.addWidget(self.tf_label)
- io_layout.addLayout(tf_layout)
- io_layout.setStretch(3, 1)
-
- adjust_layout = QtWidgets.QHBoxLayout()
- adjust_layout.addWidget(self.exposure_label)
- adjust_layout.addWidget(self.exposure_box)
- adjust_layout.setStretch(1, 2)
- adjust_layout.addWidget(self.gamma_label)
- adjust_layout.addWidget(self.gamma_box)
- adjust_layout.setStretch(3, 2)
- adjust_layout.addWidget(self.sample_precision_label)
- adjust_layout.addWidget(self.sample_precision_box)
- adjust_layout.setStretch(5, 1)
-
- image_plane_layout = QtWidgets.QVBoxLayout()
- image_plane_layout.setSpacing(0)
- image_plane_layout.addWidget(self.info_bar)
- image_plane_layout.addWidget(self.image_plane)
- image_plane_layout.addWidget(self.inspect_bar)
+ "QFrame#base_image_viewer__status_bar { background-color: black; }"
+ )
+ self.inspect_bar.setLayout(self.inspect_layout)
+
+ # Edit mode
+ self.tf_layout = QtWidgets.QHBoxLayout()
+ self.tf_layout.setContentsMargins(0, 0, 0, 0)
+ self.tf_layout.setSpacing(0)
+ self.tf_layout.addWidget(self.tf_box)
+ self.tf_layout.setStretch(0, 1)
+ self.tf_layout.addWidget(self.tf_direction_button)
+
+ self.tf_layout_outer = QtWidgets.QHBoxLayout()
+ self.tf_layout_outer.setContentsMargins(0, 0, 0, 0)
+ self.tf_layout_outer.addWidget(self.tf_label)
+ self.tf_layout_outer.setStretch(0, 0)
+ self.tf_layout_outer.addLayout(self.tf_layout)
+ self.tf_layout_outer.setStretch(1, 1)
+
+ self.tf_frame = QtWidgets.QFrame()
+ self.tf_frame.setLayout(self.tf_layout_outer)
+
+ self.inspect_layout.addWidget(
+ self.output_tf_direction_label, 1, 5, QtCore.Qt.AlignRight
+ )
+ # Preview mode
+ self.display_view_layout = QtWidgets.QHBoxLayout()
+ self.display_view_layout.setContentsMargins(0, 0, 0, 0)
+ self.display_view_layout.setSpacing(0)
+ self.display_view_layout.addWidget(self.display_box)
+ self.display_view_layout.addWidget(self.view_box)
+
+ self.display_view_layout_outer = QtWidgets.QHBoxLayout()
+ self.display_view_layout_outer.setContentsMargins(0, 0, 0, 0)
+ self.display_view_layout_outer.addWidget(self.display_view_label)
+ self.display_view_layout_outer.setStretch(0, 0)
+ self.display_view_layout_outer.addLayout(self.display_view_layout)
+ self.display_view_layout_outer.setStretch(1, 1)
+
+ self.display_view_frame = QtWidgets.QFrame()
+ self.display_view_frame.setLayout(self.display_view_layout_outer)
+
+ # Mode switch stack
+ self.mode_stack = QtWidgets.QStackedWidget()
+ self.mode_stack.addWidget(self.tf_frame) # Edit mode
+ self.mode_stack.addWidget(self.display_view_frame) # Preview mode
+
+ # Input/output
+ self.io_layout = QtWidgets.QHBoxLayout()
+ self.io_layout.addWidget(self.input_color_space_label)
+ self.io_layout.addWidget(self.input_color_space_box)
+ self.io_layout.setStretch(1, 1)
+ self.io_layout.addWidget(self.mode_stack)
+ self.io_layout.setStretch(2, 1)
+
+ # Image adjustments
+ self.adjust_layout = QtWidgets.QHBoxLayout()
+ self.adjust_layout.addWidget(self.exposure_label)
+ self.adjust_layout.addWidget(self.exposure_box)
+ self.adjust_layout.setStretch(1, 2)
+ self.adjust_layout.addWidget(self.gamma_label)
+ self.adjust_layout.addWidget(self.gamma_box)
+ self.adjust_layout.setStretch(3, 2)
+ self.adjust_layout.addWidget(self.sample_precision_label)
+ self.adjust_layout.addWidget(self.sample_precision_box)
+ self.adjust_layout.setStretch(5, 1)
+
+ # Viewport
+ self.image_plane_layout = QtWidgets.QVBoxLayout()
+ self.image_plane_layout.setSpacing(0)
+ self.image_plane_layout.addWidget(self.info_bar)
+ self.image_plane_layout.addWidget(self.image_plane)
+ self.image_plane_layout.addWidget(self.inspect_bar)
+
+ # Main layout
layout = QtWidgets.QVBoxLayout()
- layout.addLayout(io_layout)
- layout.addLayout(adjust_layout)
- layout.addLayout(image_plane_layout)
+ layout.addLayout(self.io_layout)
+ layout.addLayout(self.adjust_layout)
+ layout.addLayout(self.image_plane_layout)
+ layout.setStretch(2, 1)
self.setLayout(layout)
# Connect signals/slots
+ # ---------------------------------------------------------------------
+
self.image_plane.image_loaded.connect(self._on_image_loaded)
self.image_plane.scale_changed.connect(self._on_scale_changed)
self.image_plane.sample_changed.connect(self._on_sample_changed)
- self.image_plane.tf_subscription_requested.connect(
- self._on_tf_subscription_requested
- )
- self.input_color_space_box.currentTextChanged[str].connect(
+ self.input_color_space_box.color_space_changed.connect(
self._on_input_color_space_changed
)
- self.tf_box.currentIndexChanged[int].connect(self._on_transform_changed)
- self.tf_direction_button.clicked[bool].connect(self._on_inverse_check_clicked)
self.exposure_box.valueChanged.connect(self._on_exposure_changed)
self.gamma_box.valueChanged.connect(self._on_gamma_changed)
self.sample_precision_box.valueChanged.connect(
self._on_sample_precision_changed
)
+ signal_router = SignalRouter.get_instance()
+ signal_router.mode_changed.connect(self._on_mode_changed)
+
+ # Edit mode
+ self.image_plane.tf_subscription_requested.connect(
+ self._on_tf_subscription_requested
+ )
+ self.tf_box.currentIndexChanged[int].connect(
+ self._on_transform_changed
+ )
+ self.tf_direction_button.clicked[bool].connect(
+ self._on_inverse_check_clicked
+ )
+
# Initialize
- TransformManager.subscribe_to_transform_menu(self._on_transform_menu_changed)
+ # ---------------------------------------------------------------------
+
+ self._on_sample_precision_changed(self.sample_precision_box.value())
+ self._on_sample_changed(-1, -1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+
+ # Edit mode
+ TransformManager.subscribe_to_transform_menu(
+ self._on_transform_menu_changed
+ )
TransformManager.subscribe_to_transform_subscription_init(
self._on_transform_subscription_init
)
+
+ # Initialize viewport
self.update(force=True)
- self._on_sample_precision_changed(self.sample_precision_box.value())
- self._on_sample_changed(-1, -1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
- def update(self, force: bool = False) -> None:
+ def update(self, force: bool = False, update_items: bool = True) -> None:
"""
Make this image viewer the current OpenGL rendering context and
ask it to redraw.
:param force: Whether to force the image to redraw, regardless
of OCIO processor changes.
+ :param update_items: Whether to update dynamic OCIO items that
+ affect image processing.
"""
- self._update_input_color_spaces(update=False)
+ mode = OCIOViewMode.current_mode()
+ if mode == OCIOViewMode.Preview and update_items:
+ self.display_box.update_items()
+ self.view_box.update_items()
self.image_plane.update_ocio_proc(
- proc_context=ProcessorContext(
- self.input_color_space(),
- self.transform_item_type(),
- self.transform_item_name(),
- self.transform_direction(),
- ),
+ proc_context=self._make_processor_context(),
+ transform=self._make_transform(),
force_update=force,
)
@@ -310,10 +460,12 @@ def reset(self) -> None:
self.image_plane.reset_ocio_proc(update=False)
# Update widgets to match image plane
- with SignalsBlocked(self):
- self.set_transform_direction(False)
+ with self._ocio_signals_blocked():
+ self.set_transform_direction(ocio.TRANSFORM_DIR_FORWARD)
self.set_exposure(self.image_plane.exposure())
self.set_gamma(self.image_plane.gamma())
+ self.display_box.reset()
+ self.view_box.reset()
# Update input color spaces and redraw viewport
self.update()
@@ -334,6 +486,21 @@ def load_image(self, image_path: Path) -> None:
with self._ocio_signals_blocked():
self.set_input_color_space(self.image_plane.input_color_space())
+ def input_color_space(self) -> str:
+ """Get input color space name."""
+ return self.input_color_space_box.color_space_name()
+
+ def set_input_color_space(self, color_space: str) -> None:
+ """
+ Override current input color space. This controls how an input
+ image should be interpreted by OCIO. Each loaded image utilizes
+ OCIO config file rules to determine this automatically, so this
+ override only guarantees persistence for the current image.
+
+ :param color_space: OCIO color space name
+ """
+ self.input_color_space_box.set_color_space(color_space)
+
def view_channel(self, channel: int) -> None:
"""
Isolate a specific channel by its index. Specifying an out
@@ -344,28 +511,41 @@ def view_channel(self, channel: int) -> None:
"""
self.image_plane.update_ocio_proc(channel=channel)
- def input_color_space(self) -> str:
+ def exposure(self) -> float:
+ """Get exposure value."""
+ return self.exposure_box.value()
+
+ def set_exposure(self, value: float) -> None:
"""
- :return: Input color space name
+ Update viewer exposure, applied in scene_linear space prior to
+ the output transform.
+
+ :param value: Exposure value in stops
"""
- return self.input_color_space_box.currentText()
+ self.exposure_box.setValue(value)
- def set_input_color_space(self, color_space: str) -> None:
+ def gamma(self) -> float:
+ """Get gamma value."""
+ return self.gamma_box.value()
+
+ def set_gamma(self, value: float) -> None:
"""
- Override current input color space. This controls how an input
- image should be interpreted by OCIO. Each loaded image utilizes
- OCIO config file rules to determine this automatically, so this
- override only guarantees persistence for the current image.
+ Update viewer gamma, applied after the OCIO output transform.
- :param color_space: OCIO color space name
+ :param value: Gamma value used like: pow(rgb, 1/gamma)
"""
- self._update_input_color_spaces(update=False)
- self.input_color_space_box.setCurrentText(color_space)
+ self.gamma_box.setValue(value)
+
+ def display(self) -> str:
+ """Get current OCIO display."""
+ return self.display_box.currentText()
+
+ def view(self) -> str:
+ """Get current OCIO view."""
+ return self.view_box.currentText()
def transform(self) -> Optional[ocio.Transform]:
- """
- :return: Current OCIO transform
- """
+ """Get current OCIO transform."""
return self.image_plane.transform()
def set_transform(
@@ -386,25 +566,21 @@ def set_transform(
if (
slot != self._tf_subscription_slot
- or (transform_fwd is None and tf_direction == ocio.TRANSFORM_DIR_FORWARD)
- or (transform_inv is None and tf_direction == ocio.TRANSFORM_DIR_INVERSE)
+ or (
+ transform_fwd is None
+ and tf_direction == ocio.TRANSFORM_DIR_FORWARD
+ )
+ or (
+ transform_inv is None
+ and tf_direction == ocio.TRANSFORM_DIR_INVERSE
+ )
):
return
self._tf_fwd = transform_fwd
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,
- )
+ self.update()
def clear_transform(self) -> None:
"""
@@ -420,62 +596,31 @@ def clear_transform(self) -> None:
self.image_plane.clear_transform()
- def transform_item_type(self) -> Type | None:
- """
- :return: Transform source config item type
- """
+ def transform_item_type(self) -> type | None:
+ """Get 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
- """
+ """Get 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
- """
+ """Get transform direction being viewed."""
return (
ocio.TRANSFORM_DIR_INVERSE
if self.tf_direction_button.isChecked()
else ocio.TRANSFORM_DIR_FORWARD
)
- def set_transform_direction(self, direction: ocio.TransformDirection) -> None:
+ def set_transform_direction(
+ self, direction: ocio.TransformDirection
+ ) -> None:
"""
:param direction: Set the transform direction to be viewed
"""
- self.tf_direction_button.setChecked(direction == ocio.TRANSFORM_DIR_INVERSE)
-
- def exposure(self) -> float:
- """
- :return: Exposure value
- """
- return self.exposure_box.value()
-
- def set_exposure(self, value: float) -> None:
- """
- Update viewer exposure, applied in scene_linear space prior to
- the output transform.
-
- :param value: Exposure value in stops
- """
- self.exposure_box.setValue(value)
-
- def gamma(self) -> float:
- """
- :return: Gamma value
- """
- return self.gamma_box.value()
-
- def set_gamma(self, value: float) -> None:
- """
- Update viewer gamma, applied after the OCIO output transform.
-
- :param value: Gamma value used like: pow(rgb, 1/gamma)
- """
- self.gamma_box.setValue(value)
+ self.tf_direction_button.setChecked(
+ direction == ocio.TRANSFORM_DIR_INVERSE
+ )
@contextmanager
def _ocio_signals_blocked(self) -> Generator:
@@ -487,124 +632,134 @@ def _ocio_signals_blocked(self) -> Generator:
self.input_color_space_box.blockSignals(True)
self.exposure_box.blockSignals(True)
self.gamma_box.blockSignals(True)
+ self.display_box.blockSignals(True)
+ self.view_box.blockSignals(True)
yield
self.input_color_space_box.blockSignals(False)
self.exposure_box.blockSignals(False)
self.gamma_box.blockSignals(False)
+ self.display_box.blockSignals(False)
+ self.view_box.blockSignals(False)
+
+ def _make_processor_context(self) -> ProcessorContext:
+ """Create processor context from available data."""
+ mode = OCIOViewMode.current_mode()
+ if mode == OCIOViewMode.Preview:
+ return ProcessorContext(
+ self.input_color_space(),
+ ViewModel.__item_type__,
+ self.view(),
+ ocio.TRANSFORM_DIR_FORWARD,
+ )
+ else: # Edit
+ return ProcessorContext(
+ self.input_color_space(),
+ self.transform_item_type(),
+ self.transform_item_name(),
+ self.transform_direction(),
+ )
- def _update_input_color_spaces(self, update: bool = True) -> None:
- """
- If the current color space is no longer available, reload all
- input color spaces and choose a reasonable default.
- """
- color_space_names = ConfigCache.get_color_space_names(
+ def _make_transform(self) -> Union[ocio.Transform, None]:
+ """Create viewer transform."""
+ transform = None
+ mode = OCIOViewMode.current_mode()
+
+ if mode == OCIOViewMode.Preview:
+ display = self.display()
+ view = self.view()
+
+ if display and view:
+ # Image plane expects all transforms to be relative to the current
+ # config's scene reference space.
+ transform = ocio.DisplayViewTransform(
+ src=ReferenceSpaceManager.scene_reference_space().getName(),
+ display=display,
+ view=view,
+ direction=ocio.TRANSFORM_DIR_FORWARD,
+ )
+
+ else: # Edit
+ if self._tf_fwd is not None and self._tf_inv is not None:
+ if self.transform_direction() == ocio.TRANSFORM_DIR_INVERSE:
+ return self._tf_inv
+ else:
+ return self._tf_fwd
+ else:
+ # Return no-op transform. Returning None instead results in the image
+ # plane processor being unchanged, which is problematic when switching
+ # application modes, since it will retain the previous display/view
+ # transform.
+ return ocio.ExponentTransform()
+
+ return transform
+
+ def _get_default_color_space(self) -> str:
+ """Get reasonable default color space."""
+ all_color_spaces = ConfigCache.get_color_space_names(
ocio.SEARCH_REFERENCE_SPACE_SCENE
)
+ default_color_space = ConfigCache.get_default_color_space_name()
if (
- not self.input_color_space_box.count()
- or self.input_color_space() not in color_space_names
+ default_color_space is not None
+ and default_color_space in all_color_spaces
):
- default_color_space = ConfigCache.get_default_color_space_name()
+ return default_color_space
+ elif all_color_spaces:
+ return all_color_spaces[0]
+ else:
+ return ""
- with self._ocio_signals_blocked():
- self.input_color_space_box.clear()
- self.input_color_space_box.addItems(color_space_names)
- self.input_color_space_box.setCurrentText(default_color_space)
+ def _get_displays(self) -> list[str]:
+ """Get all active OCIO displays."""
+ config = ocio.GetCurrentConfig()
+ return list(config.getDisplays())
- if update:
- self._on_input_color_space_changed(self.input_color_space())
+ def _get_default_display(self) -> str:
+ """Get default OCIO display."""
+ config = ocio.GetCurrentConfig()
+ return config.getDefaultDisplay()
- def _on_transform_menu_changed(
- self, menu_items: list[tuple[int, str, QtGui.QIcon]]
- ) -> None:
+ def _get_views(self) -> list[str]:
"""
- Called to refresh transform menu items, and either reselect the
- existing subscription, or deselect any subscription.
+ Get all active OCIO views, given the current input color space.
"""
- target_index = -1
- current_slot = -1
- if self.tf_box.count():
- 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)
- 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 = index # Offset for "Passthrough" item
-
- # Restore previous item?
- if target_index != -1:
- self.tf_box.setCurrentIndex(target_index)
-
- # Switch to "Passthrough" if previous slot not found
- if target_index == -1 and self.tf_box.count():
- with SignalsBlocked(self.tf_box):
- self.tf_box.setCurrentIndex(0)
-
- # Force update transform
- self._on_transform_changed(0)
+ config = ocio.GetCurrentConfig()
+ input_color_space = self.input_color_space()
+ if input_color_space:
+ return config.getViews(self.display(), input_color_space)
+ else:
+ return config.getViews(self.display())
- def _on_transform_subscription_init(self, slot: int) -> None:
+ def _get_default_view(self) -> str:
"""
- If this viewer is not subscribed to a specific transform
- subscription slot, subscribe to the first slot to receive a
- transform subscription.
-
- :param slot: Transform subscription slot
+ Get default OCIO view, given the current display and input
+ color space.
"""
- if self._tf_subscription_slot == -1:
- index = self.tf_box.findData(slot, role=self.ROLE_SLOT)
- if index != -1:
- self.tf_box.setCurrentIndex(index)
-
- @QtCore.Slot(int)
- def _on_transform_changed(self, index: int) -> None:
- if index == 0:
- TransformManager.unsubscribe_from_all_transforms(self.set_transform)
- self.clear_transform()
+ config = ocio.GetCurrentConfig()
+ input_color_space = None
+ if input_color_space:
+ return config.getDefaultView(self.display(), input_color_space)
else:
- self._tf_subscription_slot = self.tf_box.currentData(role=self.ROLE_SLOT)
- TransformManager.subscribe_to_transforms_at(
- self._tf_subscription_slot, self.set_transform
- )
+ return config.getDefaultView(self.display())
- @QtCore.Slot(int)
- 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, role=self.ROLE_SLOT))
- )
+ def _on_mode_changed(self) -> None:
+ """Called when the application mode changes."""
+ mode = OCIOViewMode.current_mode()
- @QtCore.Slot(bool)
- def _on_inverse_check_clicked(self, checked: bool) -> None:
- self.set_transform(self._tf_subscription_slot, self._tf_fwd, self._tf_inv)
- if self.tf_direction_button.isChecked():
- self.tf_direction_button.setIcon(self._tf_direction_inverse_icon)
- self.tf_direction_button.setToolTip("Transform direction: Inverse")
- # Use 'minus' character to match the width of "+"
- self.output_tf_direction_label.setText("\u2212")
- else:
- self.tf_direction_button.setIcon(self._tf_direction_forward_icon)
- self.tf_direction_button.setToolTip("Transform direction: Forward")
- self.output_tf_direction_label.setText("+")
+ if mode == OCIOViewMode.Preview:
+ self.mode_stack.setCurrentWidget(self.display_view_frame)
+ else: # Edit
+ self.mode_stack.setCurrentWidget(self.tf_frame)
+
+ self.output_tf_direction_label.setVisible(mode == OCIOViewMode.Edit)
+ self.update(force=True)
@QtCore.Slot(Path, int, int)
- def _on_image_loaded(self, image_path: Path, width: int, height: int) -> None:
+ def _on_image_loaded(
+ self, image_path: Path, width: int, height: int
+ ) -> None:
self.image_name_label.setText(
self.FMT_GRAY_LABEL.format(v=image_path.as_posix())
)
@@ -669,15 +824,9 @@ def _on_sample_changed(
)
)
- @QtCore.Slot(str)
- def _on_input_color_space_changed(self, input_color_space: str) -> None:
+ def _on_input_color_space_changed(self) -> None:
self.image_plane.update_ocio_proc(
- proc_context=ProcessorContext(
- input_color_space,
- self.transform_item_type(),
- self.transform_item_name(),
- self.transform_direction(),
- )
+ proc_context=self._make_processor_context()
)
@QtCore.Slot(float)
@@ -688,6 +837,119 @@ def _on_exposure_changed(self, value: float) -> None:
def _on_gamma_changed(self, value: float) -> None:
self.image_plane.update_gamma(value)
+ @QtCore.Slot(int)
+ def _on_display_changed(self, index: int) -> None:
+ """Called when the display changes."""
+ with SignalsBlocked(self.view_box):
+ self.view_box.update_items()
+ self._on_view_changed(0)
+
+ @QtCore.Slot(int)
+ def _on_view_changed(self, index: int) -> None:
+ """Called when the view changes."""
+ self.update(update_items=False)
+
@QtCore.Slot(int)
def _on_sample_precision_changed(self, value: float) -> None:
self._sample_format = f"{{v:.{value}f}}"
+
+ def _on_transform_menu_changed(
+ self, menu_items: list[tuple[int, str, QtGui.QIcon]]
+ ) -> None:
+ """
+ Called to refresh transform menu items, and either reselect the
+ existing subscription, or deselect any subscription.
+ """
+ target_index = -1
+ current_slot = -1
+ if self.tf_box.count():
+ 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)
+ 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 = index # Offset for "Passthrough" item
+
+ # Restore previous item?
+ if target_index != -1:
+ self.tf_box.setCurrentIndex(target_index)
+
+ # Switch to "Passthrough" if previous slot not found
+ if target_index == -1 and self.tf_box.count():
+ with SignalsBlocked(self.tf_box):
+ self.tf_box.setCurrentIndex(0)
+
+ # Force update transform
+ self._on_transform_changed(0)
+
+ def _on_transform_subscription_init(self, slot: int) -> None:
+ """
+ If this viewer is not subscribed to a specific transform
+ subscription slot, subscribe to the first slot to receive a
+ transform subscription.
+
+ :param slot: Transform subscription slot
+ """
+ if self._tf_subscription_slot == -1:
+ index = self.tf_box.findData(slot, role=self.ROLE_SLOT)
+ if index != -1:
+ self.tf_box.setCurrentIndex(index)
+
+ @QtCore.Slot(int)
+ def _on_transform_changed(self, index: int) -> None:
+ if index == 0:
+ TransformManager.unsubscribe_from_all_transforms(
+ self.set_transform
+ )
+ self.clear_transform()
+ else:
+ self._tf_subscription_slot = self.tf_box.currentData(
+ role=self.ROLE_SLOT
+ )
+ TransformManager.subscribe_to_transforms_at(
+ self._tf_subscription_slot, self.set_transform
+ )
+
+ @QtCore.Slot(int)
+ 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, role=self.ROLE_SLOT))
+ )
+
+ @QtCore.Slot(bool)
+ def _on_inverse_check_clicked(self, checked: bool) -> None:
+ self.set_transform(
+ self._tf_subscription_slot, self._tf_fwd, self._tf_inv
+ )
+ if self.tf_direction_button.isChecked():
+ self.tf_direction_button.setIcon(self._tf_direction_inverse_icon)
+ self.tf_direction_button.setToolTip("Transform direction: Inverse")
+ # Use 'minus' character to match the width of "+"
+ self.output_tf_direction_label.setText("\u2212")
+ else:
+ self.tf_direction_button.setIcon(self._tf_direction_forward_icon)
+ self.tf_direction_button.setToolTip("Transform direction: Forward")
+ self.output_tf_direction_label.setText("+")
diff --git a/src/apps/ocioview/ocioview/viewer_dock.py b/src/apps/ocioview/ocioview/viewer_dock.py
index b295fc35c8..243e94363c 100644
--- a/src/apps/ocioview/ocioview/viewer_dock.py
+++ b/src/apps/ocioview/ocioview/viewer_dock.py
@@ -26,10 +26,19 @@ class ViewerDock(TabbedDockWidget):
def __init__(
self,
recent_images_menu: QtWidgets.QMenu,
+ corner_widget: Optional[QtWidgets.QWidget] = None,
parent: Optional[QtCore.QObject] = None,
):
+ """
+ :param recent_images_menu: Menu for managing recent images
+ :param corner_widget: Optional widget to place on the right
+ side of the dock title bar.
+ """
super().__init__(
- "Viewer", get_glyph_icon("mdi6.image-filter-center-focus"), parent=parent
+ "Viewer",
+ get_glyph_icon("mdi6.image-filter-center-focus"),
+ corner_widget=corner_widget,
+ parent=parent,
)
self._recent_images_menu = recent_images_menu
@@ -48,7 +57,9 @@ def __init__(
# Initialize
self._update_recent_images_menu()
- def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
+ def eventFilter(
+ self, watched: QtCore.QObject, event: QtCore.QEvent
+ ) -> bool:
"""Tab context menu implementation."""
if watched == self._tab_bar:
if event.type() == QtCore.QEvent.ContextMenu:
@@ -193,7 +204,9 @@ def _get_recent_image_paths(self) -> list[Path]:
num_images = settings.beginReadArray(self.SETTING_RECENT_IMAGES)
for i in range(num_images):
settings.setArrayIndex(i)
- recent_image_path_str = settings.value(self.SETTING_RECENT_IMAGE_PATH)
+ recent_image_path_str = settings.value(
+ self.SETTING_RECENT_IMAGE_PATH
+ )
if recent_image_path_str:
recent_image_path = Path(recent_image_path_str)
if recent_image_path.is_file():
@@ -215,7 +228,7 @@ def _add_recent_image_path(self, image_path: Path) -> None:
image_paths.insert(0, image_path)
if len(image_paths) > 10:
- image_paths = image_path[:10]
+ image_paths = image_paths[:10]
settings.beginWriteArray(self.SETTING_RECENT_IMAGES)
for i, recent_image_path in enumerate(image_paths):
diff --git a/src/apps/ocioview/ocioview/widgets/__init__.py b/src/apps/ocioview/ocioview/widgets/__init__.py
index 0ce1766718..9f0bf2e0e4 100644
--- a/src/apps/ocioview/ocioview/widgets/__init__.py
+++ b/src/apps/ocioview/ocioview/widgets/__init__.py
@@ -2,7 +2,12 @@
# Copyright Contributors to the OpenColorIO Project.
from .check_box import CheckBox
-from .combo_box import ComboBox, EnumComboBox, CallbackComboBox
+from .combo_box import (
+ ComboBox,
+ EnumComboBox,
+ CallbackComboBox,
+ ColorSpaceComboBox,
+)
from .layout import FormLayout
from .line_edit import (
LineEdit,
diff --git a/src/apps/ocioview/ocioview/widgets/combo_box.py b/src/apps/ocioview/ocioview/widgets/combo_box.py
index b2a95fe26b..d8c63d29e7 100644
--- a/src/apps/ocioview/ocioview/widgets/combo_box.py
+++ b/src/apps/ocioview/ocioview/widgets/combo_box.py
@@ -1,11 +1,18 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright Contributors to the OpenColorIO Project.
+from __future__ import annotations
+
import enum
-from typing import Callable, Optional
+from contextlib import contextmanager
+from functools import partial
+from typing import Callable, Optional, Union
+import PyOpenColorIO as ocio
from PySide6 import QtCore, QtGui, QtWidgets
+from ..config_cache import ConfigCache
+from ..signal_router import SignalRouter
from ..utils import SignalsBlocked
@@ -133,12 +140,8 @@ def update_items(self) -> str:
:return: Current item string
"""
# Get current state
- current_item = None
- if not self.count():
- if self._get_default_item is not None:
- current_item = self._get_default_item()
- else:
- current_item = self.currentText()
+ current_item = self.currentText()
+ current_item_restored = False
# Reload all items
with SignalsBlocked(self):
@@ -156,11 +159,13 @@ def update_items(self) -> str:
else:
self.addItems(self._get_items())
- # Restore original state
- index = self.findText(current_item)
- if index != -1:
- self.setCurrentIndex(index)
- elif self._get_default_item is not None:
+ # Restore original state
+ index = self.findText(current_item)
+ if index != -1:
+ self.setCurrentIndex(index)
+ current_item_restored = True
+
+ if not current_item_restored and self._get_default_item is not None:
self.setCurrentText(self._get_default_item())
return self.currentText()
@@ -183,3 +188,264 @@ def showPopup(self) -> None:
def reset(self) -> None:
super().reset()
self.update_items()
+
+
+class ColorSpaceComboBox(QtWidgets.QPushButton):
+ """
+ Combo box which maintains a menu of all active color spaces in the
+ current config.
+ """
+
+ color_space_changed = QtCore.Signal()
+
+ def __init__(
+ self,
+ reference_space_type: Optional[ocio.SearchReferenceSpaceType] = None,
+ include_roles: bool = False,
+ include_use_display_name: bool = False,
+ parent: Optional[QtCore.QObject] = None,
+ ):
+ """
+ :param reference_space_type: Optional reference space type.
+ Defaults to all reference spaces.
+ :param include_roles: Whether to include a 'Roles' sub-menu
+ :param include_use_display_name: Whether to include a special
+ '' item, used when creating shared views.
+ """
+ super().__init__(parent)
+ self.setStyleSheet("text-align: left; padding-left: 4px;")
+ self.setMinimumHeight(24)
+
+ if reference_space_type is None:
+ reference_space_type = ocio.SEARCH_REFERENCE_SPACE_ALL
+
+ self._reference_space_type = reference_space_type
+ self._include_roles = include_roles
+ self._include_use_display_name = include_use_display_name
+ self._config_cache_id = None
+ self._menu = QtWidgets.QMenu()
+ self._color_space_actions: dict[str, QtGui.QAction] = {}
+ self._value: str | None = None
+
+ # Initialize menu
+ self.update_color_spaces()
+ self._start_external_updates()
+
+ # DataWidgetMapper user property interface
+ @QtCore.Property(str, user=True)
+ def __data(self) -> str:
+ return self.color_space_name()
+
+ @__data.setter
+ def __data(self, data: str) -> None:
+ with SignalsBlocked(self):
+ self.set_color_space(data)
+
+ def set_color_space(
+ self, color_space_or_name: Union[ocio.ColorSpace, str]
+ ) -> bool:
+ """
+ Set the selected color space.
+
+ :param color_space_or_name: Color space object or name
+ :return: Whether the color space was selected
+ """
+ # Handle special shared view case
+ if color_space_or_name == ocio.OCIO_VIEW_USE_DISPLAY_NAME:
+ if self._include_use_display_name:
+ # Complete selection
+ self._commit_value(ocio.OCIO_VIEW_USE_DISPLAY_NAME)
+ return True
+ else:
+ return False
+
+ # Detect argument type
+ if isinstance(color_space_or_name, ocio.ColorSpace):
+ color_space_name = color_space_or_name.getName()
+ else:
+ color_space_name = color_space_or_name
+
+ # Verify color space
+ config = ocio.GetCurrentConfig()
+ color_space = config.getColorSpace(color_space_name)
+ if color_space is not None:
+ # Uncheck all color spaces
+ for other_action in self._color_space_actions.values():
+ other_action.setChecked(False)
+
+ # Check selected color space
+ action = self._color_space_actions.get(color_space_name)
+ if action is None:
+ return False
+ else:
+ action.setChecked(True)
+
+ # Complete selection
+ self._commit_value(color_space_name, action.text())
+ return True
+ else:
+ return False
+
+ def color_space(self) -> Union[ocio.ColorSpace, str, None]:
+ """
+ Get the selected color space.
+
+ :return: Color space object, or None if no color space is
+ selected. `OCIO_VIEW_USE_DISPLAY_NAME` may ne returned
+ if 'include_use_display_name' was True on initialization.
+ """
+ # Handle special shared view case
+ if (
+ self._include_use_display_name
+ and self._value == ocio.OCIO_VIEW_USE_DISPLAY_NAME
+ ):
+ return ocio.OCIO_VIEW_USE_DISPLAY_NAME
+
+ # Lookup and return the color space instance
+ config = ocio.GetCurrentConfig()
+ if self._value:
+ return config.getColorSpace(self._value)
+ else:
+ return None
+
+ def color_space_name(self) -> str | None:
+ """
+ Get the selected color space name.
+
+ :return: Color space name
+ """
+ # Handle special shared view case
+ if (
+ self._include_use_display_name
+ and self._value == ocio.OCIO_VIEW_USE_DISPLAY_NAME
+ ):
+ return ocio.OCIO_VIEW_USE_DISPLAY_NAME
+
+ config = ocio.GetCurrentConfig()
+
+ # Is value a role?
+ if config.hasRole(self._value):
+ return self._value
+
+ # Make sure value still references a color space
+ color_space = self.color_space()
+ if color_space is not None:
+ return color_space.getName()
+ else:
+ return None
+
+ def update_color_spaces(self) -> None:
+ """Reload color spaces from the current config."""
+ config_cache_id = ConfigCache.get_cache_id()
+ if ConfigCache.validate() and config_cache_id == self._config_cache_id:
+ return
+
+ self._config_cache_id = config_cache_id
+
+ # Preserve existing selection if possible
+ current_name = self.color_space_name()
+ current_name_available = False
+
+ # Delete previous menu and its actions
+ self._color_space_actions.clear()
+ prev_menu = self._menu
+ if prev_menu is not None:
+ prev_menu.deleteLater()
+
+ # Replace menu
+ self._menu = QtWidgets.QMenu()
+ self.setMenu(self._menu)
+
+ # Add special shared view action
+ if self._include_use_display_name:
+ action = self._menu.addAction(
+ ocio.OCIO_VIEW_USE_DISPLAY_NAME,
+ partial(self.set_color_space, ocio.OCIO_VIEW_USE_DISPLAY_NAME),
+ )
+ action.setCheckable(True)
+ self._color_space_actions[ocio.OCIO_VIEW_USE_DISPLAY_NAME] = action
+ self._menu.addSeparator()
+
+ # Configure color space menu helper
+ config = ocio.GetCurrentConfig()
+
+ menu_params = ocio.ColorSpaceMenuParameters(config)
+ menu_params.setIncludeColorSpaces()
+ menu_params.setSearchReferenceSpaceType(self._reference_space_type)
+ menu_params.setIncludeRoles(self._include_roles)
+
+ # Build menu hierarchy
+ menu_helper = ocio.ColorSpaceMenuHelper(menu_params)
+
+ for i in range(menu_helper.getNumColorSpaces()):
+ name = menu_helper.getName(i)
+ label = menu_helper.getUIName(i)
+ family = menu_helper.getFamily(i)
+ description = menu_helper.getDescription(i)
+
+ if name == current_name:
+ current_name_available = True
+
+ if family == "Roles":
+ self._menu.addSeparator()
+
+ parent_menu = self._menu
+ for level in menu_helper.getHierarchyLevels(i):
+ child_menu = parent_menu.findChild(
+ QtWidgets.QMenu,
+ level,
+ options=QtCore.Qt.FindDirectChildrenOnly,
+ )
+ if child_menu is None:
+ child_menu = parent_menu.addMenu(level)
+ child_menu.setObjectName(level)
+ parent_menu = child_menu
+
+ # Add color space action
+ action = parent_menu.addAction(
+ label, partial(self.set_color_space, name)
+ )
+ action.setToolTip(description)
+ action.setCheckable(True)
+ self._color_space_actions[name] = action
+
+ # Restore previous selection or select a reasonable default
+ if current_name_available:
+ self.set_color_space(current_name)
+ else:
+ default_name = ConfigCache.get_default_color_space_name()
+ if default_name:
+ self.set_color_space(default_name)
+ else:
+ self.setText("")
+
+ def _commit_value(self, value: str, label: Optional[str] = None) -> None:
+ """Commit color space value and broadcast to listeners."""
+ with self._external_updates_paused():
+ self._value = value
+ self.setText(label or value)
+ self.color_space_changed.emit()
+
+ def _start_external_updates(self) -> None:
+ """Start color space updates from external config changes."""
+ signal_router = SignalRouter.get_instance()
+ signal_router.config_reloaded.connect(self.update_color_spaces)
+ signal_router.color_spaces_changed.connect(self.update_color_spaces)
+ signal_router.roles_changed.connect(self.update_color_spaces)
+
+ def _stop_external_updates(self) -> None:
+ """Stop color space updates from external config changes."""
+ signal_router = SignalRouter.get_instance()
+ signal_router.config_reloaded.disconnect(self.update_color_spaces)
+ signal_router.color_spaces_changed.disconnect(self.update_color_spaces)
+ signal_router.roles_changed.disconnect(self.update_color_spaces)
+
+ @contextmanager
+ def _external_updates_paused(self) -> None:
+ """
+ Context manager to pause color space updates from external
+ config changes within the enclosed scope.
+ """
+ self._stop_external_updates()
+ yield
+ self._start_external_updates()
diff --git a/src/apps/ocioview/ocioview/widgets/structure.py b/src/apps/ocioview/ocioview/widgets/structure.py
index f5fe7d3054..d4f49b61a1 100644
--- a/src/apps/ocioview/ocioview/widgets/structure.py
+++ b/src/apps/ocioview/ocioview/widgets/structure.py
@@ -15,11 +15,17 @@ class DockTitleBar(QtWidgets.QFrame):
"""Dock widget title bar widget with icon."""
def __init__(
- self, title: str, icon: QtGui.QIcon, parent: Optional[QtCore.QObject] = None
+ self,
+ title: str,
+ icon: QtGui.QIcon,
+ widget: Optional[QtWidgets.QWidget] = None,
+ parent: Optional[QtCore.QObject] = None,
):
"""
:param title: Title text
:param icon: Dock icon
+ :param widget: Optional widget to display opposite the title
+ and icon.
"""
super().__init__(parent=parent)
@@ -33,6 +39,7 @@ def __init__(
self.icon = QtWidgets.QLabel()
self.icon.setPixmap(icon.pixmap(ICON_SIZE_ITEM))
self.title = QtWidgets.QLabel(title)
+ self.widget = widget
# Layout
inner_layout = QtWidgets.QHBoxLayout()
@@ -41,6 +48,8 @@ def __init__(
inner_layout.addWidget(self.icon)
inner_layout.addWidget(self.title)
inner_layout.addStretch()
+ if widget is not None:
+ inner_layout.addWidget(self.widget)
inner_frame = QtWidgets.QFrame()
inner_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
@@ -65,14 +74,20 @@ class TabbedDockWidget(QtWidgets.QDockWidget):
_tab_icons = {}
def __init__(
- self, title: str, icon: QtGui.QIcon, parent: Optional[QtCore.QObject] = None
+ self,
+ title: str,
+ icon: QtGui.QIcon,
+ corner_widget: Optional[QtWidgets.QWidget] = None,
+ parent: Optional[QtCore.QObject] = None,
):
"""
:param title: Title text
:param icon: Dock icon
+ :param corner_widget: Optional widget to place on the right
+ side of the dock title bar.
"""
super().__init__(parent=parent)
- self.setTitleBarWidget(DockTitleBar(title, icon))
+ self.setTitleBarWidget(DockTitleBar(title, icon, widget=corner_widget))
self.setFeatures(
QtWidgets.QDockWidget.DockWidgetMovable
| QtWidgets.QDockWidget.DockWidgetFloatable
@@ -144,7 +159,9 @@ def _rotate_icon(
return QtGui.QIcon(pixmap)
@QtCore.Slot(QtCore.Qt.DockWidgetArea)
- def _on_dock_location_changed(self, area: QtCore.Qt.DockWidgetArea) -> None:
+ def _on_dock_location_changed(
+ self, area: QtCore.Qt.DockWidgetArea
+ ) -> None:
"""
Adjust tab icons to always orient upward on dock area move.
"""
@@ -211,5 +228,6 @@ def _on_current_changed(self, index: int) -> None:
)
else:
widget.setSizePolicy(
- QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored
+ QtWidgets.QSizePolicy.Ignored,
+ QtWidgets.QSizePolicy.Ignored,
)
diff --git a/src/apps/ocioview/pyproject.toml b/src/apps/ocioview/pyproject.toml
index ca3e2453fc..858223b4d5 100644
--- a/src/apps/ocioview/pyproject.toml
+++ b/src/apps/ocioview/pyproject.toml
@@ -13,13 +13,13 @@ imageio = ">= 2, < 3"
networkx = ">= 2.7, < 3"
numpy = ">= 1.22, < 2"
opencolorio = "*"
-qtawesome = "*"
pygfx = "*"
-wpgu = "*"
pygments = "*"
pyopengl = "*"
pyside6 = "*"
+qtawesome = "*"
scipy = ">= 1.8, < 2"
+wpgu = "*"
[tool.poetry.group.dev.dependencies]
black = "*"
@@ -33,6 +33,7 @@ pre-commit = "*"
pyright = "*"
pytest = "*"
pytest-cov = "*"
+pytest-qt = "*"
pytest-xdist = "*"
ruff = "*"
toml = "*"
diff --git a/src/apps/ocioview/requirements.txt b/src/apps/ocioview/requirements.txt
index 55d31e852e..124acd49da 100644
--- a/src/apps/ocioview/requirements.txt
+++ b/src/apps/ocioview/requirements.txt
@@ -1,12 +1,13 @@
-OpenColorIO
-PyOpenGL
-PySide6
-QtAwesome
colour-science @ git+https://github.com/colour-science/colour.git
colour-visuals @ git+https://github.com/colour-science/colour-visuals.git
imageio
networkx
numpy
+OpenColorIO
pygfx
pygments
+PyOpenGL
+PySide6
+QtAwesome
+scipy
wgpu
diff --git a/src/apps/ocioview/tests/conftest.py b/src/apps/ocioview/tests/conftest.py
new file mode 100644
index 0000000000..94240b71ac
--- /dev/null
+++ b/src/apps/ocioview/tests/conftest.py
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright Contributors to the OpenColorIO Project.
+
+import pytest
+
+
+@pytest.fixture(scope="session")
+def qapp(qapp):
+ """
+ pytest-qt qapp fixture override, with ocioview-specific setup
+ steps.
+ """
+ from ocioview.setup import setup_app
+
+ setup_app(qapp)
+ return qapp
+
+
+@pytest.fixture
+def qtbot(qapp, qtbot):
+ """
+ pytest-qt qtbot fixture override, injecting the overridden qapp
+ fixture before qtbot can initialize the default implementation.
+ """
+ return qtbot
+
+
+@pytest.fixture(scope="session")
+def ocio():
+ import PyOpenColorIO as ocio
+
+ return ocio
+
+
+@pytest.fixture
+def ocio_view(ocio, qtbot):
+ from ocioview.main_window import OCIOView
+
+ ocio_view = OCIOView(transient=True)
+ ocio_view.show()
+ qtbot.addWidget(ocio_view)
+
+ return ocio_view
+
+
+@pytest.fixture
+def ocio_config(ocio):
+ """
+ .. note::
+ This fixture should be used AFTER the `ocio_view` fixture,
+ since `OCIOView` instantiation resets the current config,
+ invalidating any existing references.
+ """
+ return ocio.GetCurrentConfig()
diff --git a/src/apps/ocioview/tests/items/test_color_space_edit.py b/src/apps/ocioview/tests/items/test_color_space_edit.py
new file mode 100644
index 0000000000..596265cf61
--- /dev/null
+++ b/src/apps/ocioview/tests/items/test_color_space_edit.py
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright Contributors to the OpenColorIO Project.
+
+
+def test_add_and_remove_color_space(ocio_view, ocio_config):
+ color_space_count = len(ocio_config.getColorSpaces())
+ assert ocio_config.getColorSpace("ColorSpace_1") is None
+
+ edit = ocio_view.config_dock.color_space_edit
+ edit.list.add_button.click()
+
+ assert ocio_config.getColorSpace("ColorSpace_1") is not None
+ assert len(ocio_config.getColorSpaces()) == color_space_count + 1
+
+ edit.list.remove_button.click()
+
+ assert ocio_config.getColorSpace("ColorSpace_1") is None
+ assert len(ocio_config.getColorSpaces()) == color_space_count
+
+
+def test_rename_color_space(ocio_view, ocio_config):
+ edit = ocio_view.config_dock.color_space_edit
+ edit.list.add_button.click()
+ color_space_count = len(ocio_config.getColorSpaces())
+
+ assert ocio_config.getColorSpace("ColorSpace_1") is not None
+ assert ocio_config.getColorSpace("test") is None
+
+ edit.param_edit.name_edit.set_value("test")
+ edit.mapper.submit()
+
+ assert ocio_config.getColorSpace("ColorSpace_1") is None
+ assert ocio_config.getColorSpace("test") is not None
+ assert len(ocio_config.getColorSpaces()) == color_space_count
+
+
+def test_edit_color_space_reference_space_type(ocio, ocio_view, ocio_config):
+ edit = ocio_view.config_dock.color_space_edit
+ edit.list.add_button.click()
+
+ assert (
+ ocio_config.getColorSpace("ColorSpace_1").getReferenceSpaceType()
+ == ocio.REFERENCE_SPACE_SCENE
+ )
+
+ edit.param_edit.reference_space_type_combo.set_member(
+ ocio.REFERENCE_SPACE_DISPLAY
+ )
+ edit.mapper.submit()
+
+ assert (
+ ocio_config.getColorSpace("ColorSpace_1").getReferenceSpaceType()
+ == ocio.REFERENCE_SPACE_DISPLAY
+ )