From 2ee0464b9adf90ead8fd5143813f1bcb1b9a2a4f Mon Sep 17 00:00:00 2001 From: Michael Dolan Date: Sun, 14 Jun 2026 22:50:56 -0400 Subject: [PATCH] Remove ocioview Signed-off-by: Michael Dolan --- .gitignore | 2 - docs/guides/using_ocio/tool_overview.rst | 4 +- docs/releases/ocio_2_3.rst | 3 + src/apps/ocioview/README.md | 50 - .../ocioview/icons/opencolorio-icon-color.svg | 1 - src/apps/ocioview/main.py | 17 - src/apps/ocioview/ocioview/__init__.py | 2 - src/apps/ocioview/ocioview/config_cache.py | 516 ------- src/apps/ocioview/ocioview/config_dock.py | 180 --- src/apps/ocioview/ocioview/constants.py | 36 - .../ocioview/ocioview/inspect/__init__.py | 7 - .../inspect/chromaticities_inspector.py | 598 -------- .../ocioview/inspect/code_inspector.py | 254 ---- .../ocioview/inspect/curve_inspector.py | 805 ----------- .../ocioview/inspect/log_inspector.py | 86 -- src/apps/ocioview/ocioview/inspect_dock.py | 77 - src/apps/ocioview/ocioview/items/__init__.py | 11 - .../items/active_display_view_edit.py | 87 -- .../items/active_display_view_model.py | 213 --- .../ocioview/items/color_space_edit.py | 221 --- .../ocioview/items/color_space_model.py | 295 ---- .../ocioview/items/config_item_edit.py | 460 ------ .../ocioview/items/config_item_model.py | 830 ----------- .../ocioview/items/config_properties_edit.py | 151 -- .../ocioview/items/config_properties_model.py | 166 --- src/apps/ocioview/ocioview/items/delegates.py | 192 --- .../ocioview/ocioview/items/display_model.py | 255 ---- .../ocioview/items/display_view_edit.py | 124 -- .../ocioview/ocioview/items/file_rule_edit.py | 203 --- .../ocioview/items/file_rule_model.py | 434 ------ src/apps/ocioview/ocioview/items/look_edit.py | 70 - .../ocioview/ocioview/items/look_model.py | 224 --- .../ocioview/items/named_transform_edit.py | 115 -- .../ocioview/items/named_transform_model.py | 230 --- src/apps/ocioview/ocioview/items/role_edit.py | 76 - .../ocioview/ocioview/items/role_model.py | 110 -- src/apps/ocioview/ocioview/items/rule_edit.py | 90 -- .../ocioview/items/shared_view_edit.py | 92 -- .../ocioview/items/shared_view_model.py | 213 --- src/apps/ocioview/ocioview/items/utils.py | 178 --- src/apps/ocioview/ocioview/items/view_edit.py | 378 ----- .../ocioview/ocioview/items/view_model.py | 563 -------- .../ocioview/items/view_transform_edit.py | 126 -- .../ocioview/items/view_transform_model.py | 239 --- .../ocioview/items/viewing_rule_edit.py | 186 --- .../ocioview/items/viewing_rule_model.py | 312 ---- src/apps/ocioview/ocioview/log_handlers.py | 78 - src/apps/ocioview/ocioview/main_window.py | 655 --------- src/apps/ocioview/ocioview/message_router.py | 428 ------ src/apps/ocioview/ocioview/mode.py | 46 - .../ocioview/ocioview/processor_context.py | 28 - .../ocioview/ocioview/ref_space_manager.py | 177 --- src/apps/ocioview/ocioview/settings.py | 10 - src/apps/ocioview/ocioview/setup.py | 81 -- src/apps/ocioview/ocioview/signal_router.py | 85 -- src/apps/ocioview/ocioview/style.py | 117 -- .../ocioview/ocioview/transform_manager.py | 341 ----- .../ocioview/ocioview/transforms/__init__.py | 22 - .../ocioview/transforms/allocation_edit.py | 69 - .../ocioview/transforms/builtin_edit.py | 48 - .../ocioview/ocioview/transforms/cdl_edit.py | 66 - .../ocioview/transforms/color_space_edit.py | 61 - .../ocioview/transforms/display_view_edit.py | 114 -- .../ocioview/transforms/exponent_edit.py | 47 - .../transforms/exponent_with_linear_edit.py | 55 - .../transforms/exposure_contrast_edit.py | 78 - .../ocioview/ocioview/transforms/file_edit.py | 72 - .../transforms/fixed_function_edit.py | 86 -- .../ocioview/transforms/log_affine_edit.py | 62 - .../ocioview/transforms/log_camera_edit.py | 82 -- .../ocioview/ocioview/transforms/log_edit.py | 39 - .../ocioview/ocioview/transforms/look_edit.py | 74 - .../ocioview/transforms/matrix_edit.py | 144 -- .../ocioview/transforms/range_edit.py | 134 -- .../ocioview/transforms/transform_edit.py | 259 ---- .../transforms/transform_edit_factory.py | 86 -- .../transforms/transform_edit_stack.py | 352 ----- .../ocioview/ocioview/transforms/utils.py | 28 - src/apps/ocioview/ocioview/undo.py | 206 --- src/apps/ocioview/ocioview/utils.py | 328 ----- src/apps/ocioview/ocioview/viewer/__init__.py | 6 - .../ocioview/ocioview/viewer/image_plane.py | 1281 ----------------- .../ocioview/ocioview/viewer/image_viewer.py | 955 ------------ .../ocioview/viewer/offscreen_viewer.py | 160 -- src/apps/ocioview/ocioview/viewer/utils.py | 89 -- src/apps/ocioview/ocioview/viewer_dock.py | 251 ---- .../ocioview/ocioview/widgets/__init__.py | 24 - .../ocioview/ocioview/widgets/check_box.py | 28 - .../ocioview/ocioview/widgets/combo_box.py | 451 ------ .../ocioview/ocioview/widgets/item_view.py | 246 ---- src/apps/ocioview/ocioview/widgets/layout.py | 12 - .../ocioview/ocioview/widgets/line_edit.py | 509 ------- .../ocioview/ocioview/widgets/list_widget.py | 402 ------ .../ocioview/ocioview/widgets/log_view.py | 91 -- .../ocioview/ocioview/widgets/structure.py | 233 --- .../ocioview/ocioview/widgets/table_widget.py | 378 ----- .../ocioview/ocioview/widgets/text_edit.py | 72 - src/apps/ocioview/pyproject.toml | 203 --- src/apps/ocioview/requirements.txt | 12 - src/apps/ocioview/tests/conftest.py | 54 - .../tests/items/test_color_space_edit.py | 54 - 101 files changed, 6 insertions(+), 19545 deletions(-) delete mode 100644 src/apps/ocioview/README.md delete mode 100644 src/apps/ocioview/icons/opencolorio-icon-color.svg delete mode 100644 src/apps/ocioview/main.py delete mode 100644 src/apps/ocioview/ocioview/__init__.py delete mode 100644 src/apps/ocioview/ocioview/config_cache.py delete mode 100644 src/apps/ocioview/ocioview/config_dock.py delete mode 100644 src/apps/ocioview/ocioview/constants.py delete mode 100644 src/apps/ocioview/ocioview/inspect/__init__.py delete mode 100644 src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py delete mode 100644 src/apps/ocioview/ocioview/inspect/code_inspector.py delete mode 100644 src/apps/ocioview/ocioview/inspect/curve_inspector.py delete mode 100644 src/apps/ocioview/ocioview/inspect/log_inspector.py delete mode 100644 src/apps/ocioview/ocioview/inspect_dock.py delete mode 100644 src/apps/ocioview/ocioview/items/__init__.py delete mode 100644 src/apps/ocioview/ocioview/items/active_display_view_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/active_display_view_model.py delete mode 100644 src/apps/ocioview/ocioview/items/color_space_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/color_space_model.py delete mode 100644 src/apps/ocioview/ocioview/items/config_item_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/config_item_model.py delete mode 100644 src/apps/ocioview/ocioview/items/config_properties_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/config_properties_model.py delete mode 100644 src/apps/ocioview/ocioview/items/delegates.py delete mode 100644 src/apps/ocioview/ocioview/items/display_model.py delete mode 100644 src/apps/ocioview/ocioview/items/display_view_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/file_rule_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/file_rule_model.py delete mode 100644 src/apps/ocioview/ocioview/items/look_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/look_model.py delete mode 100644 src/apps/ocioview/ocioview/items/named_transform_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/named_transform_model.py delete mode 100644 src/apps/ocioview/ocioview/items/role_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/role_model.py delete mode 100644 src/apps/ocioview/ocioview/items/rule_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/shared_view_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/shared_view_model.py delete mode 100644 src/apps/ocioview/ocioview/items/utils.py delete mode 100644 src/apps/ocioview/ocioview/items/view_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/view_model.py delete mode 100644 src/apps/ocioview/ocioview/items/view_transform_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/view_transform_model.py delete mode 100644 src/apps/ocioview/ocioview/items/viewing_rule_edit.py delete mode 100644 src/apps/ocioview/ocioview/items/viewing_rule_model.py delete mode 100644 src/apps/ocioview/ocioview/log_handlers.py delete mode 100644 src/apps/ocioview/ocioview/main_window.py delete mode 100644 src/apps/ocioview/ocioview/message_router.py delete mode 100644 src/apps/ocioview/ocioview/mode.py delete mode 100644 src/apps/ocioview/ocioview/processor_context.py delete mode 100644 src/apps/ocioview/ocioview/ref_space_manager.py delete mode 100644 src/apps/ocioview/ocioview/settings.py delete mode 100644 src/apps/ocioview/ocioview/setup.py delete mode 100644 src/apps/ocioview/ocioview/signal_router.py delete mode 100644 src/apps/ocioview/ocioview/style.py delete mode 100644 src/apps/ocioview/ocioview/transform_manager.py delete mode 100644 src/apps/ocioview/ocioview/transforms/__init__.py delete mode 100644 src/apps/ocioview/ocioview/transforms/allocation_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/builtin_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/cdl_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/color_space_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/display_view_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/exponent_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/exponent_with_linear_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/exposure_contrast_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/file_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/fixed_function_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/log_affine_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/log_camera_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/log_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/look_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/matrix_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/range_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/transform_edit.py delete mode 100644 src/apps/ocioview/ocioview/transforms/transform_edit_factory.py delete mode 100644 src/apps/ocioview/ocioview/transforms/transform_edit_stack.py delete mode 100644 src/apps/ocioview/ocioview/transforms/utils.py delete mode 100644 src/apps/ocioview/ocioview/undo.py delete mode 100644 src/apps/ocioview/ocioview/utils.py delete mode 100644 src/apps/ocioview/ocioview/viewer/__init__.py delete mode 100644 src/apps/ocioview/ocioview/viewer/image_plane.py delete mode 100644 src/apps/ocioview/ocioview/viewer/image_viewer.py delete mode 100644 src/apps/ocioview/ocioview/viewer/offscreen_viewer.py delete mode 100644 src/apps/ocioview/ocioview/viewer/utils.py delete mode 100644 src/apps/ocioview/ocioview/viewer_dock.py delete mode 100644 src/apps/ocioview/ocioview/widgets/__init__.py delete mode 100644 src/apps/ocioview/ocioview/widgets/check_box.py delete mode 100644 src/apps/ocioview/ocioview/widgets/combo_box.py delete mode 100644 src/apps/ocioview/ocioview/widgets/item_view.py delete mode 100644 src/apps/ocioview/ocioview/widgets/layout.py delete mode 100644 src/apps/ocioview/ocioview/widgets/line_edit.py delete mode 100644 src/apps/ocioview/ocioview/widgets/list_widget.py delete mode 100644 src/apps/ocioview/ocioview/widgets/log_view.py delete mode 100644 src/apps/ocioview/ocioview/widgets/structure.py delete mode 100644 src/apps/ocioview/ocioview/widgets/table_widget.py delete mode 100644 src/apps/ocioview/ocioview/widgets/text_edit.py delete mode 100644 src/apps/ocioview/pyproject.toml delete mode 100644 src/apps/ocioview/requirements.txt delete mode 100644 src/apps/ocioview/tests/conftest.py delete mode 100644 src/apps/ocioview/tests/items/test_color_space_edit.py diff --git a/.gitignore b/.gitignore index 827d8df759..02c8f8b813 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,3 @@ mastercopy .idea/ .venv/ .vscode/ - -src/apps/ocioview/uv.lock diff --git a/docs/guides/using_ocio/tool_overview.rst b/docs/guides/using_ocio/tool_overview.rst index 040d0b397d..8461ee49bc 100644 --- a/docs/guides/using_ocio/tool_overview.rst +++ b/docs/guides/using_ocio/tool_overview.rst @@ -347,7 +347,9 @@ ocioview This is a new GUI tool for inspecting and editing config files. It is currently an alpha release and we are looking for contributors to extend it or provide tutorials. -Please see the README in apps/ocioview for details about installation. +Please see the README in the +`ocioview repository `_ +for details about installation. .. _overview-ociowrite: diff --git a/docs/releases/ocio_2_3.rst b/docs/releases/ocio_2_3.rst index 4f627ad0dd..1e679a212b 100644 --- a/docs/releases/ocio_2_3.rst +++ b/docs/releases/ocio_2_3.rst @@ -24,6 +24,9 @@ the 2.3.0 release, ``ocioview`` is currently in an alpha release (please see the the apps/ocioview directory for details). Documentation and tutorials will be forthcoming and we are looking for contributors to help with this and other tasks. +Note: ``ocioview`` has moved to a +`dedicated repository. `_ + Built-in Configs **************** diff --git a/src/apps/ocioview/README.md b/src/apps/ocioview/README.md deleted file mode 100644 index a62465307a..0000000000 --- a/src/apps/ocioview/README.md +++ /dev/null @@ -1,50 +0,0 @@ - - - -ocioview (alpha) -================ - -**Work in progress**. ``ocioview`` is a visual editor for OCIO configs, written in -Python. - -The app currently consists of three main components; a viewer, a config editor, and a -transform and config inspector. Multiple viewers can be loaded in different tabs. The -config editor is a tabbed model/view interface for the current config. Models for -each config item type interface directly with the config in memory. The inspector -presents interfaces for inspecting processor curves, serialized config YAML, CTF and -shader code, and the OCIO log. - -The app's scene file is a config. This design allows dynamic interconnectivity between -config items, reducing risk of errors during config authoring. Undo/redo stack support -for most features is also implemented. - -These components are linked with 10 possible transform subscriptions. Each subscription -tracks the transform(s) for one config item, and each viewer can subscribe to any of -these transforms, providing fast visual feedback for transform editing. - -``ocioview`` being an alpha release means this app is functional, but still in -development, so may have some rough edges. Development has mostly been done on Windows. -Improved support for other platforms is forthcoming. Feedback and bug reports are -appreciated. - -An ``ocioview`` demo was given at the 2023 OCIO Virtual Town Hall meeting, which can be -viewed on the [ASWF YouTube channel here](https://www.youtube.com/watch?v=y-oq693Wl8g). - -Usage ------ - -1. Install dependencies on ``PYTHONPATH`` -2. Run ``python ocioview.py`` - -Dependencies ------------- - -* PyOpenColorIO -* [OpenImageIO (Python bindings)](https://github.com/OpenImageIO/oiio) -* ``pip install -r requirements.txt`` - * [numpy](https://pypi.org/project/numpy/) - * [Pygments](https://pypi.org/project/Pygments/) - * [PyOpenGL](https://pypi.org/project/PyOpenGL/) - * [PySide6](https://pypi.org/project/PySide6/) - * [QtAwesome](https://pypi.org/project/QtAwesome/) - * [imageio](https://pypi.org/project/imageio/) diff --git a/src/apps/ocioview/icons/opencolorio-icon-color.svg b/src/apps/ocioview/icons/opencolorio-icon-color.svg deleted file mode 100644 index 594fa912b4..0000000000 --- a/src/apps/ocioview/icons/opencolorio-icon-color.svg +++ /dev/null @@ -1 +0,0 @@ -ocio_dot_m \ No newline at end of file diff --git a/src/apps/ocioview/main.py b/src/apps/ocioview/main.py deleted file mode 100644 index 15d897ab5d..0000000000 --- a/src/apps/ocioview/main.py +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import sys - -from ocioview.main_window import OCIOView -from ocioview.setup import setup_app - - -if __name__ == "__main__": - app = setup_app() - - # Start ocioview - ocio_view = OCIOView() - ocio_view.show() - - sys.exit(app.exec_()) diff --git a/src/apps/ocioview/ocioview/__init__.py b/src/apps/ocioview/ocioview/__init__.py deleted file mode 100644 index faca5ef88b..0000000000 --- a/src/apps/ocioview/ocioview/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. diff --git a/src/apps/ocioview/ocioview/config_cache.py b/src/apps/ocioview/ocioview/config_cache.py deleted file mode 100644 index eb25d3d35c..0000000000 --- a/src/apps/ocioview/ocioview/config_cache.py +++ /dev/null @@ -1,516 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import re -import uuid -import warnings -from typing import Callable, Optional, Union - -import PyOpenColorIO as ocio - - -class ConfigCache: - """ - Helper function result cache, tied to the current Config cache ID. - """ - - _cache_id: str = None - _callbacks: list[Callable] = [] - - _active_displays: Optional[list[str]] = None - _active_views: Optional[list[str]] = None - _all_names: Optional[list[str]] = None - _categories: Optional[list[str]] = None - _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 - _displays: Optional[list[str]] = None - _encodings: Optional[list[str]] = None - _equality_groups: Optional[list[str]] = None - _families: Optional[list[str]] = None - _looks: Optional[list[ocio.Look]] = None - _named_transforms: Optional[list[ocio.NamedTransform]] = None - _shared_views: Optional[list[str]] = None - _views: dict[ - tuple[Optional[str], Optional[str], Optional[ocio.ViewType]], list[str] - ] = {} - _view_transforms: Optional[list[ocio.ViewTransform]] = None - _view_transform_names: Optional[list[str]] = None - _viewing_rule_names: Optional[list[str]] = None - - @classmethod - def get_cache_id(cls) -> tuple[str, bool]: - """ - Try to get the cache ID for the current config. If the config - is in an invalid state this will fail and a random config ID - will be generated (which will be invalidated on the next config - cache validation). - - :return: Tuple of cache ID, and whether the config is valid - """ - config = ocio.GetCurrentConfig() - - try: - return config.getCacheID(), True - except Exception as e: - # Invalid config state; generate random cache ID - warnings.warn(str(e)) - return uuid.uuid4().hex, False - - @classmethod - def register_reset_callback(cls, callback: Callable) -> None: - """ - :param callback: Callable to call whenever the cache is - cleared, which should reset external caches tied to this - class' cache lifetime. - """ - cls._callbacks.append(callback) - - @classmethod - def validate(cls) -> bool: - """ - Check cache validity, resetting all caches if the config has - changed. - - :return: Whether cache is still valid. If False, the calling - function should re-pull data from the current config and - update the cache. - """ - cache_id, is_valid = cls.get_cache_id() - - if cache_id != cls._cache_id: - cls._active_displays = None - cls._active_views = None - cls._all_names = None - cls._categories = None - cls._color_spaces.clear() - cls._color_space_names.clear() - cls._default_color_space_name = None - cls._default_view_transform_name = None - cls._displays = None - cls._encodings = None - cls._equality_groups = None - cls._families = None - cls._looks = None - cls._named_transforms = None - cls._shared_views = None - cls._views.clear() - cls._view_transforms = None - cls._view_transform_names = None - cls._viewing_rule_names = None - - for callback in cls._callbacks: - callback() - - cls._cache_id = cache_id - return False - - return True - - @classmethod - def get_active_displays(cls) -> list[str]: - """ - :return: List of active displays from the current config - """ - if not cls.validate() or cls._active_displays is None: - cls._active_displays = list(ocio.GetCurrentConfig().getActiveDisplays()) - - return cls._active_displays - - @classmethod - def get_active_views(cls) -> list[str]: - """ - :return: List of active views from the current config - """ - if not cls.validate() or cls._active_views is None: - cls._active_views = list(ocio.GetCurrentConfig().getActiveViews()) - - return cls._active_views - - @classmethod - def get_all_names(cls) -> list[str]: - """ - :return: All unique names from the current config. When creating - any new config object or adding aliases or roles, there should - be no intersection with the returned names. - """ - if not cls.validate() or cls._all_names is None: - config = ocio.GetCurrentConfig() - color_spaces = cls.get_color_spaces() - named_transforms = cls.get_named_transforms() - - cls._all_names = ( - [c.getName() for c in color_spaces] - + [a for c in color_spaces for a in c.getAliases()] - + [t.getName() for t in named_transforms] - + [a for t in named_transforms for a in t.getAliases()] - + list(config.getLookNames()) - + list(config.getRoleNames()) - + cls.get_view_transform_names() - ) - - return cls._all_names - - @classmethod - def get_builtin_color_space_roles( - cls, include_deprecated: bool = False - ) -> list[str]: - """ - Get role names which are defined by the core OCIO library. - - :param include_deprecated: By default, deprecated roles are omitted - from the returned role list. Set to True to return all builtin - roles. - :return: list of role names - """ - roles = [ - ocio.ROLE_DATA, - ocio.ROLE_DEFAULT, - ocio.ROLE_COLOR_PICKING, - ocio.ROLE_COLOR_TIMING, - ocio.ROLE_COMPOSITING_LOG, - ocio.ROLE_INTERCHANGE_DISPLAY, - ocio.ROLE_INTERCHANGE_SCENE, - ocio.ROLE_MATTE_PAINT, - ocio.ROLE_RENDERING, - ocio.ROLE_SCENE_LINEAR, - ocio.ROLE_TEXTURE_PAINT, - ] - if include_deprecated: - roles.extend([ocio.ROLE_REFERENCE]) - return roles - - @classmethod - def get_categories(cls) -> list[str]: - """ - :return: All color space/view transform/named transform categories - from the current config. - """ - if not cls.validate() or cls._categories is None: - categories = set() - - for color_space in cls.get_color_spaces(): - categories.update(color_space.getCategories()) - for view_tf in cls.get_view_transforms(): - categories.update(view_tf.getCategories()) - for named_tf in cls.get_named_transforms(): - categories.update(named_tf.getCategories()) - - cls._categories = sorted(filter(None, categories)) - - return cls._categories - - @classmethod - def get_color_spaces( - 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 - """ - 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( - reference_space_type, visibility - ) - if as_set: - color_space_set = ocio.ColorSpaceSet() - for color_space in color_spaces: - color_space_set.addColorSpace(color_space) - cls._color_spaces[cache_key] = color_space_set - else: - cls._color_spaces[cache_key] = list(color_spaces) - - return cls._color_spaces[cache_key] - - @classmethod - def get_color_space_names( - cls, - reference_space_type: ocio.SearchReferenceSpaceType = ocio.SEARCH_REFERENCE_SPACE_ALL, - ) -> list[str]: - """ - :param reference_space_type: Optional reference space search type - to limit results. Searches all reference spaces by default. - :return: Requested color space names from the current config - """ - cache_key = reference_space_type - - 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 - ) - ) - - return cls._color_space_names[cache_key] - - @classmethod - def get_default_color_space_name(cls) -> Optional[str]: - """ - Choose a reasonable default color space from the current config. - - :return: Color space name, or None if there are no color spaces - """ - if not cls.validate() or cls._default_color_space_name is None: - config = ocio.GetCurrentConfig() - - # Check common roles - for role in (ocio.ROLE_DEFAULT, ocio.ROLE_SCENE_LINEAR): - color_space = config.getColorSpace(role) - if color_space is not None: - break - - if color_space is None: - # Get first active color space - for color_space in config.getColorSpaces(): - break - - if color_space is None: - # Get first color space, active or not - for color_space in config.getColorSpaces( - ocio.SEARCH_REFERENCE_SPACE_ALL, ocio.COLORSPACE_ALL - ): - break - - if color_space is not None: - cls._default_color_space_name = color_space.getName() - else: - cls._default_color_space_name = None - - return cls._default_color_space_name - - @classmethod - def get_default_view_transform_name(cls) -> Optional[str]: - """ - :return: Default view transform name from the current config - """ - if not cls.validate() or cls._default_view_transform_name is None: - config = ocio.GetCurrentConfig() - default_view_transform_name = config.getDefaultViewTransformName() - - if not default_view_transform_name: - view_transform_names = cls.get_view_transform_names() - if view_transform_names: - default_view_transform_name = view_transform_names[0] - - cls._default_view_transform_name = default_view_transform_name - - return cls._default_view_transform_name - - @classmethod - def get_displays(cls) -> list[str]: - """ - :return: Sorted list of OCIO displays from the current config - """ - if not cls.validate() or cls._displays is None: - cls._displays = list(ocio.GetCurrentConfig().getDisplaysAll()) - - return cls._displays - - @classmethod - def get_encodings(cls) -> list[str]: - """ - :return: All color space/named transform encodings in current - config. - """ - if not cls.validate() or cls._encodings is None: - # Pre-defined standard encodings from the OCIO docs - encodings = { - "scene-linear", - "display-linear", - "log", - "sdr-video", - "hdr-video", - "data", - } - for color_space in cls.get_color_spaces(): - encodings.add(color_space.getEncoding()) - for named_tf in cls.get_named_transforms(): - encodings.add(named_tf.getEncoding()) - - cls._encodings = sorted(filter(None, encodings)) - - return cls._encodings - - @classmethod - def get_equality_groups(cls) -> list[str]: - """ - :return: All color space families in current config - """ - if not cls.validate() or cls._equality_groups is None: - equality_groups = set() - - for color_space in cls.get_color_spaces(): - equality_groups.add(color_space.getEqualityGroup()) - - cls._equality_groups = sorted(filter(None, equality_groups)) - - return cls._equality_groups - - @classmethod - def get_families(cls) -> list[str]: - """ - :return: All color space/view transform/named transform families - from the current config. - """ - if not cls.validate() or cls._families is None: - families = set() - - for color_space in cls.get_color_spaces(): - families.add(color_space.getFamily()) - for view_tf in cls.get_view_transforms(): - families.add(view_tf.getFamily()) - for named_tf in cls.get_named_transforms(): - families.add(named_tf.getFamily()) - - cls._families = sorted(filter(None, families)) - - return cls._families - - @classmethod - def get_looks(cls) -> list[ocio.Look]: - """ - :return: All looks from the current config - """ - if not cls.validate() or cls._looks is None: - cls._looks = list(ocio.GetCurrentConfig().getLooks()) - - return cls._looks - - @classmethod - def get_named_transforms(cls) -> list[ocio.NamedTransform]: - """ - :return: All named transforms from the current config - """ - if not cls.validate() or cls._named_transforms is None: - cls._named_transforms = list( - ocio.GetCurrentConfig().getNamedTransforms( - ocio.NAMEDTRANSFORM_ALL - ) - ) - - return cls._named_transforms - - @classmethod - def get_shared_views(cls) -> list[str]: - """ - :return: All shared views for the current config - """ - if not cls.validate() or cls._shared_views is None: - cls._shared_views = list(ocio.GetCurrentConfig().getSharedViews()) - - return cls._shared_views - - @classmethod - def get_views( - cls, - display: Optional[str] = None, - color_space_name: Optional[str] = None, - view_type: Optional[ocio.ViewType] = None, - ) -> list[str]: - """ - :param display: OCIO display to get views for - :param color_space_name: Contextual input color space name (for - viewing rules evaluation) - :param view_type: Optionally request ONLY shared views or - display-defined views for the requested display(s). When unset, - all view types are returned. Ignored when a color space name is - provided. - :return: Sorted list of matching OCIO views from the current config - """ - cache_key = (display, color_space_name, view_type) - - if not cls.validate() or cache_key not in cls._views: - config = ocio.GetCurrentConfig() - - if display is not None: - if color_space_name is not None: - views = config.getViews(display, color_space_name) - elif view_type is not None: - views = config.getViews(view_type, display) - else: - # Ignore active views by getting all views from each view type - views = list( - config.getViews(ocio.VIEW_DISPLAY_DEFINED, display) - ) + list(config.getViews(ocio.VIEW_SHARED, display)) - else: - # NOTE: Controlled recursion into this function - views = set() - for display in cls.get_displays(): - views.update( - cls.get_views( - display, - color_space_name=color_space_name, - view_type=view_type, - ) - ) - - cls._views[cache_key] = list(views) - - return cls._views[cache_key] - - @classmethod - 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() - ) - - return cls._view_transforms - - @classmethod - def get_view_transform_names(cls) -> list[str]: - """ - :return: Sorted list of view transform names from the current - config. - """ - if not cls.validate() or cls._view_transform_names is None: - cls._view_transform_names = sorted( - ocio.GetCurrentConfig().getViewTransformNames() - ) - - return cls._view_transform_names - - @classmethod - def get_viewing_rule_names(cls) -> list[str]: - """ - :return: Sorted list of viewing rule names from the current config - """ - 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()) - ] - ) - - return cls._viewing_rule_names diff --git a/src/apps/ocioview/ocioview/config_dock.py b/src/apps/ocioview/ocioview/config_dock.py deleted file mode 100644 index b1c8ab4c4c..0000000000 --- a/src/apps/ocioview/ocioview/config_dock.py +++ /dev/null @@ -1,180 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from .signal_router import SignalRouter -from .items import ( - ColorSpaceEdit, - ConfigPropertiesEdit, - DisplayViewEdit, - LookEdit, - NamedTransformEdit, - RoleEdit, - RuleEdit, - ViewTransformEdit, -) -from .log_handlers import message_queue -from .utils import get_glyph_icon -from .widgets import TabbedDockWidget - - -class ConfigDock(TabbedDockWidget): - """ - Dockable widget for editing the current config. - """ - - 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 = [] - - self.setAllowedAreas( - QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea - ) - self.tabs.setTabPosition(QtWidgets.QTabWidget.West) - - # Widgets - self.properties_edit = ConfigPropertiesEdit() - self._connect_config_item_model(self.properties_edit.model) - - self.role_edit = RoleEdit() - self._connect_config_item_model(self.role_edit.model) - - self.rule_edit = RuleEdit() - self._connect_config_item_model(self.rule_edit.file_rule_edit.model) - 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.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 - ) - self._connect_config_item_model( - self.display_view_edit.active_display_view_edit.active_view_edit.model - ) - - self.look_edit = LookEdit() - self._connect_config_item_model(self.look_edit.model) - - self.view_transform_edit = ViewTransformEdit() - self._connect_config_item_model(self.view_transform_edit.model) - - self.color_space_edit = ColorSpaceEdit() - self._connect_config_item_model(self.color_space_edit.model) - - self.named_transform_edit = NamedTransformEdit() - self._connect_config_item_model(self.named_transform_edit.model) - - # Layout - self.add_tab( - self.properties_edit, - self.properties_edit.item_type_label(), - self.properties_edit.item_type_icon(), - ) - self.add_tab( - self.role_edit, - f"Color Space {self.role_edit.item_type_label(plural=True)}", - self.role_edit.item_type_icon(), - ) - self.add_tab( - self.rule_edit, - self.rule_edit.item_type_label(plural=True), - self.rule_edit.item_type_icon(), - ) - self.add_tab( - self.display_view_edit, - self.display_view_edit.item_type_label(plural=True), - self.display_view_edit.item_type_icon(), - ) - self.add_tab( - self.look_edit, - self.look_edit.item_type_label(plural=True), - self.look_edit.item_type_icon(), - ) - self.add_tab( - self.view_transform_edit, - self.view_transform_edit.item_type_label(plural=True), - self.view_transform_edit.item_type_icon(), - ) - self.add_tab( - self.color_space_edit, - self.color_space_edit.item_type_label(plural=True), - self.color_space_edit.item_type_icon(), - ) - self.add_tab( - self.named_transform_edit, - self.named_transform_edit.item_type_label(plural=True), - self.named_transform_edit.item_type_icon(), - ) - - # Initialize - self.update_config_views() - - def reset(self) -> None: - """Reset data for all config item models.""" - for model in self._models: - model.reset() - - self.update_config_views() - - def update_config_views(self) -> None: - """ - Push the current OCIO config into the message queue to give - any listening config code view(s) an update. - - .. note:: - Views can also connect to the config_changed signal, but - since the message queue is needed to HTML format the config - YAML data, this short circuits that trip with a direct - config update for relevant views. - """ - message_queue.put_nowait(ocio.GetCurrentConfig()) - - def _connect_config_item_model( - self, model: QtCore.QAbstractItemModel - ) -> None: - """ - Collect model and route all config changes to the - 'config_changed' signal. - """ - self._models.append(model) - - model.dataChanged.connect(self._on_config_changed) - model.item_added.connect(self._on_config_changed) - model.item_moved.connect(self._on_config_changed) - model.item_removed.connect(self._on_config_changed) - model.warning_raised.connect(self._on_warning_raised) - - def _on_config_changed(self, *args, **kwargs) -> None: - """ - Broadcast to the wider application that the config has changed. - """ - SignalRouter.get_instance().emit_config_changed() - self.update_config_views() - - def _on_warning_raised(self, message: str) -> None: - """Raise item model warnings in a message box.""" - QtWidgets.QMessageBox.warning(self, "Warning", message) diff --git a/src/apps/ocioview/ocioview/constants.py b/src/apps/ocioview/ocioview/constants.py deleted file mode 100644 index 7a973c7c2e..0000000000 --- a/src/apps/ocioview/ocioview/constants.py +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from pathlib import Path - -from PySide6 import QtCore, QtGui - - -# Root application directory -ROOT_DIR = Path(__file__).parent.parent - -# Sizes -ICON_SIZE_BUTTON = QtCore.QSize(18, 18) -ICON_SIZE_ITEM = QtCore.QSize(18, 18) -ICON_SIZE_TAB = QtCore.QSize(16, 16) -ICON_SCALE_FACTOR = 1.15 - -MARGIN_WIDTH = 13 # Pixels - -# Colors -BORDER_COLOR_ROLE = QtGui.QPalette.Dark -TOOL_BAR_BG_COLOR_ROLE = QtGui.QPalette.Mid -TOOL_BAR_BORDER_COLOR_ROLE = QtGui.QPalette.Midlight - -GRAY_COLOR = QtGui.QColor("dimgray") -R_COLOR = QtGui.QColor.fromHsvF(0.0, 0.5, 1.0) -G_COLOR = QtGui.QColor.fromHsvF(0.33, 0.5, 1.0) -B_COLOR = QtGui.QColor.fromHsvF(0.66, 0.5, 1.0) - -# Icons -ICONS_DIR = ROOT_DIR / "icons" -ICON_PATH_OCIO = ICONS_DIR / "opencolorio-icon-color.svg" - -# Value edit array component label sets -RGB = ("r", "g", "b") -RGBA = tuple(list(RGB) + ["a"]) diff --git a/src/apps/ocioview/ocioview/inspect/__init__.py b/src/apps/ocioview/ocioview/inspect/__init__.py deleted file mode 100644 index 6e021885bb..0000000000 --- a/src/apps/ocioview/ocioview/inspect/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from .chromaticities_inspector import ChromaticitiesInspector -from .code_inspector import CodeInspector -from .curve_inspector import CurveInspector -from .log_inspector import LogInspector diff --git a/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py b/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py deleted file mode 100644 index f030cd0009..0000000000 --- a/src/apps/ocioview/ocioview/inspect/chromaticities_inspector.py +++ /dev/null @@ -1,598 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from itertools import groupby -from typing import Optional - -import colour -import pygfx as gfx -import numpy as np -import PyOpenColorIO as ocio -from colour_visuals import ( - VisualChromaticityDiagram, - VisualGrid, - VisualRGBColourspace2D, - VisualRGBColourspace3D, - VisualRGBScatter3D, - VisualSpectralLocus2D, -) -from colour import CCS_ILLUMINANTS, RGB_Colourspace, XYZ_to_RGB -from PySide6 import QtCore, QtGui, QtWidgets - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_TAB -from ..message_router import MessageRouter -from ..processor_context import ProcessorContext -from ..utils import ( - color_space_to_rgb_colourspace, - get_glyph_icon, - subsampling_factor, -) -from ..viewer import WgpuCanvasOffScreenViewer - - -class ChromaticitiesInspector(QtWidgets.QWidget): - """ - Widget for inspecting chromaticities of the loaded image. - - The image processing from its input to chromaticities display is as follows: - - 1. The current active OCIO processor is applied to the image, converting - from the *input color space* to the *output transform color space*. - 2. The image is converted from the *chromaticities color space* to the - *CIE-XYZ-D65 interchange color space. This space must be defined in the - config. - 3. The resulting image is then converted from the *CIE-XYZ-D65 interchange - color space* to the internal :class:`VisualRGBScatter3D` class instance - working space. - - The *input and chromaticities color space* 2D and 3D gamuts are automatically - generated from the config by transforming to the *CIE-XYZ-D65 interchange - color space* and producing a :class:`colour.RGB_Colourspace` class instance. - """ - - MAXIMUM_IMAGE_SAMPLES_COUNT = 1e6 - """Maximum number of samples from the image to display.""" - COLOR_BACKGROUND = np.array([0.18, 0.18, 0.18]) - """Background color of the *WebGPU* viewer.""" - COLOR_RGB_COLORSPACE_INPUT = np.array([1, 0.5, 0.25]) - """Color associated with the input RGB colorspace.""" - COLOR_RGB_COLORSPACE_CHROMATICITIES = np.array([0.25, 1, 0.5]) - """Color associated with the chromaticities RGB colorspace.""" - - @classmethod - def label(cls) -> str: - return "Chromaticities" - - @classmethod - def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("mdi6.grain", size=ICON_SIZE_TAB) - - def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: - super().__init__(parent=parent) - - colour.utilities.filter_warnings(*[True] * 4) - colour.plotting.CONSTANTS_COLOUR_STYLE.font.size = 20 - - self._context = None - self._processor = None - self._image_array = np.atleast_3d([0, 0, 0]).astype(np.float32) - - # Chromaticity Diagram Working Space - self._working_whitepoint = CCS_ILLUMINANTS[ - "CIE 1931 2 Degree Standard Observer" - ]["D65"] - working_space = RGB_Colourspace( - "CIE-XYZ-D65", - colour.XYZ_to_xy(np.identity(3)), - self._working_whitepoint, - "D65", - use_derived_matrix_RGB_to_XYZ=True, - use_derived_matrix_XYZ_to_RGB=True, - ) - colour.RGB_COLOURSPACES[working_space.name] = working_space - self._working_space = working_space.name - - # Widgets - self._wgpu_viewer = WgpuCanvasOffScreenViewer() - self._conversion_chain_label = QtWidgets.QLabel() - self._conversion_chain_label.setStyleSheet( - ".QLabel { font-size: 10pt;qproperty-alignment: AlignCenter;}" - ) - - self._chromaticities_color_spaces_label = QtWidgets.QLabel( - "Chromaticities Color Space" - ) - self._chromaticities_color_spaces_combobox = QtWidgets.QComboBox() - self._chromaticities_color_spaces_combobox.setToolTip( - "Chromaticities Color Space" - ) - - self._method_label = get_glyph_icon("mdi.grid", as_widget=True) - self._method_label.setToolTip("Method") - self._method_combobox = QtWidgets.QComboBox() - self._method_combobox.addItems( - ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] - ) - self._method_combobox.setToolTip("Method") - - self._draw_input_color_space_pushbutton = QtWidgets.QPushButton() - self._draw_input_color_space_pushbutton.setIcon( - get_glyph_icon("mdi.import") - ) - self._draw_input_color_space_pushbutton.setCheckable(True) - self._draw_input_color_space_pushbutton.setToolTip( - "Draw Input Color Space" - ) - - self._draw_chromaticities_color_space_pushbutton = ( - QtWidgets.QPushButton() - ) - self._draw_chromaticities_color_space_pushbutton.setIcon( - get_glyph_icon("mdi.set-none") - ) - self._draw_chromaticities_color_space_pushbutton.setCheckable(True) - self._draw_chromaticities_color_space_pushbutton.setToolTip( - "Draw Chromaticities Color Space" - ) - - self._use_3d_visuals_pushbutton = QtWidgets.QPushButton() - self._use_3d_visuals_pushbutton.setIcon( - get_glyph_icon("mdi.cube-outline") - ) - self._use_3d_visuals_pushbutton.setCheckable(True) - self._use_3d_visuals_pushbutton.setToolTip("Use 3D Visuals") - - self._use_orthographic_projection_pushbutton = QtWidgets.QPushButton() - self._use_orthographic_projection_pushbutton.setIcon( - get_glyph_icon("mdi.camera-control") - ) - self._use_orthographic_projection_pushbutton.setCheckable(True) - self._use_orthographic_projection_pushbutton.setToolTip( - "Orthographic Projection" - ) - - self._reset_camera_pushbutton = QtWidgets.QPushButton() - self._reset_camera_pushbutton.setIcon(get_glyph_icon("mdi.restart")) - self._reset_camera_pushbutton.setToolTip("Reset Camera") - - # Layout - vbox_layout = QtWidgets.QVBoxLayout() - vbox_layout.addWidget(self._conversion_chain_label) - spacer = QtWidgets.QSpacerItem( - 20, - 20, - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding, - ) - vbox_layout.addItem(spacer) - self._wgpu_viewer.setLayout(vbox_layout) - - hbox_layout = QtWidgets.QHBoxLayout() - hbox_layout.addWidget(self._chromaticities_color_spaces_label) - hbox_layout.addWidget(self._chromaticities_color_spaces_combobox) - hbox_layout.setStretch(1, 1) - - hbox_layout.addWidget(self._method_label) - hbox_layout.addWidget(self._method_combobox) - - hbox_layout.addWidget(self._draw_input_color_space_pushbutton) - hbox_layout.addWidget(self._draw_chromaticities_color_space_pushbutton) - hbox_layout.addWidget(self._use_3d_visuals_pushbutton) - hbox_layout.addWidget(self._use_orthographic_projection_pushbutton) - hbox_layout.addWidget(self._reset_camera_pushbutton) - - vbox_layout = QtWidgets.QVBoxLayout() - vbox_layout.addLayout(hbox_layout) - vbox_layout.addWidget(self._wgpu_viewer) - self.setLayout(vbox_layout) - - msg_router = MessageRouter.get_instance() - msg_router.config_html_ready.connect(self._on_config_html_ready) - msg_router.processor_ready.connect(self._on_processor_ready) - msg_router.image_ready.connect(self._on_image_ready) - - # Visuals - self._wgpu_viewer.wgpu_scene.add( - gfx.Background(None, gfx.BackgroundMaterial(self.COLOR_BACKGROUND)) - ) - - method = "CIE 1931" - self._visuals = { - "grid": VisualGrid(size=4), - "spectral_locus": VisualSpectralLocus2D( - method=method, - ), - "chromaticity_diagram": VisualChromaticityDiagram( - method=method, - opacity=0.25, - ), - "rgb_color_space_input_2d": VisualRGBColourspace2D( - self._working_space, - colour=self.COLOR_RGB_COLORSPACE_INPUT, - thickness=2, - ), - "rgb_color_space_input_3d": VisualRGBColourspace3D( - self._working_space, - wireframe=True, - segments=24, - ), - "rgb_color_space_chromaticities_2d": VisualRGBColourspace2D( - self._working_space, - colour=self.COLOR_RGB_COLORSPACE_CHROMATICITIES, - thickness=2, - ), - "rgb_color_space_chromaticities_3d": VisualRGBColourspace3D( - self._working_space, - wireframe=True, - segments=24, - ), - "rgb_scatter_3d": VisualRGBScatter3D( - np.zeros(3), self._working_space, model=method, size=4 - ), - } - - self._root = gfx.Group() - for visual in self._visuals.values(): - self._root.add(visual) - - self._setup_widgets() - self._setup_visuals() - self._setup_notifications() - - @property - def wgpu_viewer(self) -> WgpuCanvasOffScreenViewer: - """ - Getter property for the *WebGPU* viewer. - - :return: *WebGPU* viewer. - """ - - return self._wgpu_viewer - - def reset(self) -> None: - """Resets the widgets and *Visuals* to their initial state.""" - - self._setup_widgets() - self._setup_visuals() - - def showEvent(self, event: QtGui.QShowEvent) -> None: - """Start listening for processor updates, if visible.""" - super().showEvent(event) - - msg_router = MessageRouter.get_instance() - # NOTE: We need to be able to receive notifications about config changes - # and this is currently the only way to do that without connecting - # to the `ConfigDock.config_changed` signal. - msg_router.config_updates_allowed = True - msg_router.processor_updates_allowed = True - msg_router.image_updates_allowed = True - - def hideEvent(self, event: QtGui.QHideEvent) -> None: - """Stop listening for processor updates, if not visible.""" - super().hideEvent(event) - - msg_router = MessageRouter.get_instance() - msg_router.config_updates_allowed = False - msg_router.processor_updates_allowed = False - msg_router.image_updates_allowed = False - - def _set_rgb_color_space_input_visuals_visibility(self) -> None: - """Set the visibility of the *input color space* related *Visuals*.""" - - use_3d_visuals = self._use_3d_visuals_pushbutton.isChecked() - draw_input_color_space = ( - self._draw_input_color_space_pushbutton.isChecked() - ) - - self._visuals["rgb_color_space_input_2d"].visible = ( - not use_3d_visuals - ) * draw_input_color_space - self._visuals["rgb_color_space_input_3d"].visible = ( - use_3d_visuals * draw_input_color_space - ) - - self._wgpu_viewer.render() - - def _set_rgb_color_space_chromaticities_visuals_visibility(self) -> None: - """Set the visibility of the *chromaticities color space* related *Visuals*.""" - - use_3d_visuals = self._use_3d_visuals_pushbutton.isChecked() - draw_chromaticities_color_space = ( - self._draw_chromaticities_color_space_pushbutton.isChecked() - ) - - self._visuals["rgb_color_space_chromaticities_2d"].visible = ( - not use_3d_visuals - ) * draw_chromaticities_color_space - self._visuals["rgb_color_space_chromaticities_3d"].visible = ( - use_3d_visuals * draw_chromaticities_color_space - ) - - self._wgpu_viewer.render() - - def _on_draw_input_color_space_pushbutton_clicked( - self, state: bool - ) -> None: - """ - Slot triggered when the `draw_input_color_space_pushbutton` widget - is clicked. - """ - - self._set_rgb_color_space_input_visuals_visibility() - - def _on_draw_chromaticities_color_space_pushbutton_clicked( - self, state: bool - ) -> None: - """ - Slot triggered when the `draw_chromaticities_color_space_pushbutton` - widget is clicked. - """ - - self._set_rgb_color_space_chromaticities_visuals_visibility() - - def _on_use_3d_visuals_pushbutton_clicked(self, state: bool) -> None: - """ - Slot triggered when the `use_3d_visuals_pushbutton` widget is clicked. - """ - - self._set_rgb_color_space_input_visuals_visibility() - self._set_rgb_color_space_chromaticities_visuals_visibility() - - def _on_use_orthographic_projection_pushbutton_clicked( - self, state: bool - ) -> None: - """ - Slot triggered when the `use_orthographic_projection_pushbutton` widget - is clicked. - """ - - self._wgpu_viewer.wgpu_camera.fov = 0 if state else 50 - - self._wgpu_viewer.render() - - def _setup_widgets(self) -> None: - """Setup the widgets initial state.""" - - self._draw_input_color_space_pushbutton.setChecked(True) - self._draw_chromaticities_color_space_pushbutton.setChecked(True) - self._use_3d_visuals_pushbutton.setChecked(False) - self._use_orthographic_projection_pushbutton.setChecked(True) - - self._set_rgb_color_space_input_visuals_visibility() - self._set_rgb_color_space_chromaticities_visuals_visibility() - - def _setup_visuals(self) -> None: - """Setup the *Visuals* initial state.""" - - self._visuals["rgb_color_space_input_2d"].visible = False - self._visuals["rgb_color_space_input_2d"].local.position = np.array( - [0, 0, 0.000025] - ) - 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_3d"].visible = False - self._visuals["rgb_scatter_3d"].visible = False - - self._wgpu_viewer.wgpu_scene.add(self._root) - - self._reset_camera() - - def _setup_notifications(self) -> None: - """Setup the widgets notifications, i.e., signals and slots.""" - - self._chromaticities_color_spaces_combobox.textActivated.connect( - self._update_visuals - ) - self._method_combobox.textActivated.connect(self._update_visuals) - self._draw_input_color_space_pushbutton.clicked.connect( - self._on_draw_input_color_space_pushbutton_clicked - ) - self._draw_chromaticities_color_space_pushbutton.clicked.connect( - self._on_draw_chromaticities_color_space_pushbutton_clicked - ) - self._use_3d_visuals_pushbutton.clicked.connect( - self._on_use_3d_visuals_pushbutton_clicked - ) - self._use_orthographic_projection_pushbutton.clicked.connect( - self._on_use_orthographic_projection_pushbutton_clicked - ) - self._reset_camera_pushbutton.clicked.connect(self._reset_camera) - - @QtCore.Slot(str) - def _on_config_html_ready(self, record: str) -> None: - """Slot triggered when the *Config* html is updated.""" - - color_space_names = ConfigCache.get_color_space_names() - - items = [ - self._chromaticities_color_spaces_combobox.itemText(i) - for i in range(self._chromaticities_color_spaces_combobox.count()) - ] - - if items != color_space_names: - self._chromaticities_color_spaces_combobox.clear() - self._chromaticities_color_spaces_combobox.addItems( - color_space_names - ) - - config = ocio.GetCurrentConfig() - has_role_interchange_display = config.hasRole( - ocio.ROLE_INTERCHANGE_DISPLAY - ) - self._chromaticities_color_spaces_combobox.setEnabled( - has_role_interchange_display - ) - - self._draw_input_color_space_pushbutton.setEnabled( - has_role_interchange_display - ) - self._set_rgb_color_space_input_visuals_visibility() - self._visuals[ - "rgb_color_space_input_2d" - ].visible *= has_role_interchange_display - self._visuals[ - "rgb_color_space_input_3d" - ].visible *= has_role_interchange_display - - self._draw_chromaticities_color_space_pushbutton.setEnabled( - has_role_interchange_display - ) - self._set_rgb_color_space_chromaticities_visuals_visibility() - self._visuals[ - "rgb_color_space_chromaticities_2d" - ].visible *= has_role_interchange_display - self._visuals[ - "rgb_color_space_chromaticities_3d" - ].visible *= has_role_interchange_display - - @QtCore.Slot(ocio.CPUProcessor) - def _on_processor_ready( - self, proc_context: ProcessorContext, cpu_proc: ocio.CPUProcessor - ) -> None: - """ - Slot triggered when the *OCIO* processor is ready. - - The processor and context are stored and can then be used to process - the image when it is itself ready. - """ - - self._context = proc_context - self._processor = cpu_proc - - self._update_visuals() - - @QtCore.Slot(np.ndarray) - def _on_image_ready(self, image_array: np.ndarray) -> None: - """ - Slot triggered when the image is ready. - """ - - sub_sampling_factor = int( - np.sqrt( - subsampling_factor( - image_array, self.MAXIMUM_IMAGE_SAMPLES_COUNT - ) - ) - ) - self._image_array = image_array[ - ::sub_sampling_factor, ::sub_sampling_factor - ] - - self._visuals["rgb_scatter_3d"].visible = True - - self._update_visuals() - - def _update_visuals(self, *args): - """ - Update the *Visuals* to the desired state. - """ - - 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 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, - self._context.transform_item_name, - ] - - rgb_colourspace = color_space_to_rgb_colourspace( - self._context.input_color_space - ) - - 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._processor.applyRGB(image_array) - - # 2. Convert from chromaticities input space to "CIE-XYZ-D65" interchange - config = ocio.GetCurrentConfig() - input_color_space = ( - self._chromaticities_color_spaces_combobox.currentText() - ) - if ( - config.hasRole(ocio.ROLE_INTERCHANGE_DISPLAY) - and input_color_space in ConfigCache.get_color_space_names() - ): - chromaticities_colorspace = ( - self._chromaticities_color_spaces_combobox.currentText() - ) - conversion_chain += [ - chromaticities_colorspace, - ocio.ROLE_INTERCHANGE_DISPLAY.replace( - "cie_xyz_d65_interchange", "CIE-XYZ-D65" - ), - ] - - rgb_colourspace = color_space_to_rgb_colourspace( - chromaticities_colorspace - ) - - if rgb_colourspace is not None: - self._visuals[ - "rgb_color_space_chromaticities_2d" - ].colourspace = rgb_colourspace - self._visuals[ - "rgb_color_space_chromaticities_3d" - ].colourspace = rgb_colourspace - - colorspace_transform = ocio.ColorSpaceTransform( - src=chromaticities_colorspace, - dst=ocio.ROLE_INTERCHANGE_DISPLAY, - ) - processor = config.getProcessor( - colorspace_transform, ocio.TRANSFORM_DIR_FORWARD - ).getDefaultCPUProcessor() - processor.applyRGB(image_array) - - # 3. Convert from "CIE-XYZ-D65" to "VisualRGBScatter3D" working space - conversion_chain += ["CIE-XYZ-D65", self._working_space] - - 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) - ] - - if len(conversion_chain) == 1: - conversion_chain = [] - - self._conversion_chain_label.setText(" → ".join(conversion_chain)) - - self._visuals["rgb_scatter_3d"].RGB = image_array - - method = self._method_combobox.currentText() - self._visuals["spectral_locus"].method = method - self._visuals["chromaticity_diagram"].method = method - self._visuals["rgb_color_space_input_2d"].method = method - self._visuals["rgb_color_space_input_3d"].model = method - self._visuals["rgb_color_space_chromaticities_2d"].method = method - self._visuals["rgb_color_space_chromaticities_3d"].model = method - self._visuals["rgb_scatter_3d"].model = method - - self._wgpu_viewer.render() - - def _reset_camera(self) -> None: - self._wgpu_viewer.wgpu_camera.fov = 0 - self._wgpu_viewer.wgpu_camera.local.position = np.array([0.5, 0.5, 2]) - self._wgpu_viewer.wgpu_camera.show_pos(np.array([0.5, 0.5, 0.5])) - - self._wgpu_viewer.render() diff --git a/src/apps/ocioview/ocioview/inspect/code_inspector.py b/src/apps/ocioview/ocioview/inspect/code_inspector.py deleted file mode 100644 index 1e932c2db7..0000000000 --- a/src/apps/ocioview/ocioview/inspect/code_inspector.py +++ /dev/null @@ -1,254 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from contextlib import contextmanager -from typing import Optional - -import PyOpenColorIO as ocio -from pygments.formatters import HtmlFormatter -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_TAB -from ..message_router import MessageRouter -from ..utils import get_glyph_icon, processor_to_shader_html -from ..widgets import EnumComboBox, LogView - - -class CodeInspector(QtWidgets.QWidget): - """ - Widget for inspecting OCIO related code, which updates - asynchronously when visible, to reduce unnecessary - background processing. - """ - - @classmethod - def label(cls) -> str: - return "Code" - - @classmethod - def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("mdi6.code-json", size=ICON_SIZE_TAB) - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Source objects for CTF and shader views - self._prev_group_tf = None - self._prev_gpu_proc = None - - # HTML style - palette = self.palette() - - html_css = HtmlFormatter(style="material").get_style_defs() - # Update line number colors to match palette - html_css = html_css.replace( - "#263238", palette.color(palette.ColorRole.Base).name() - ) - html_css = html_css.replace( - "#37474F", palette.color(palette.ColorRole.Text).darker(150).name() - ) - - # Widgets - self.config_view = LogView() - self.config_view.document().setDefaultStyleSheet(html_css) - - 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.released.connect(self._on_export_button_released) - - self.ctf_view = LogView() - self.ctf_view.document().setDefaultStyleSheet(html_css) - 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.currentIndexChanged[int].connect( - self._on_gpu_language_changed - ) - - self.shader_view = LogView() - self.shader_view.document().setDefaultStyleSheet(html_css) - self.shader_view.prepend_tool_bar_widget(self.gpu_language_box) - - # Layout - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab( - self.config_view, - get_glyph_icon("mdi6.code-json", size=ICON_SIZE_TAB), - "Config", - ) - self.tabs.addTab( - self.ctf_view, - get_glyph_icon("mdi6.code-tags", size=ICON_SIZE_TAB), - "Processor (CTF)", - ) - self.tabs.addTab( - self.shader_view, - get_glyph_icon("mdi6.dots-grid", size=ICON_SIZE_TAB), - "Processor (Shader)", - ) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.tabs) - self.setLayout(layout) - - # Initialize - msg_router = MessageRouter.get_instance() - msg_router.config_html_ready.connect(self._on_config_html_ready) - msg_router.ctf_html_ready.connect(self._on_ctf_html_ready) - msg_router.shader_html_ready.connect(self._on_shader_html_ready) - - self.tabs.currentChanged.connect(self._on_tab_changed) - - def showEvent(self, event: QtGui.QShowEvent) -> None: - """ - Start listening for code updates for the current tab, if - visible. - """ - super().showEvent(event) - self._on_tab_changed(self.tabs.currentIndex()) - - def hideEvent(self, event: QtGui.QHideEvent) -> None: - """ - Stop listening for code updates for all tabs, if not visible. - """ - super().hideEvent(event) - self._on_tab_changed(-1) - - def reset(self) -> None: - """Clear all code.""" - self.config_view.reset() - self.shader_view.reset() - self.ctf_view.reset() - - @contextmanager - def _scroll_preserved(self, log_view: LogView) -> None: - """ - Context manager to preserve viewport scroll/cursor position - through text/html update. - - :param log_view: Log view widget to preserve scroll for - """ - v_scroll_bar = log_view.verticalScrollBar() - 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_line_num = prev_cursor.blockNumber() - - # Get scroll bar positions - v_scroll_pos = v_scroll_bar.value() - h_scroll_pos = h_scroll_bar.value() - - # Replace text/html - yield - - # Restore current line number - cursor = QtGui.QTextCursor(log_view.document()) - cursor.movePosition( - QtGui.QTextCursor.Down, - QtGui.QTextCursor.MoveAnchor, - prev_line_num - 1, - ) - log_view.setTextCursor(cursor) - - # Restore scroll positions - v_scroll_bar.setValue(v_scroll_pos) - h_scroll_bar.setValue(h_scroll_pos) - - @QtCore.Slot(str) - def _on_config_html_ready(self, record: str) -> None: - """ - Update config view to show the current OCIO config's YAML - source. - """ - with self._scroll_preserved(self.config_view): - self.config_view.setHtml(record) - - @QtCore.Slot(str, ocio.GroupTransform) - 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. - """ - self._prev_group_tf = group_tf - - with self._scroll_preserved(self.ctf_view): - self.ctf_view.setHtml(record) - - @QtCore.Slot(str, ocio.GPUProcessor) - 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. - """ - self._prev_gpu_proc = gpu_proc - - with self._scroll_preserved(self.shader_view): - self.shader_view.setHtml(record) - - @QtCore.Slot(int) - def _on_gpu_language_changed(self, index: int) -> None: - """ - Update shader language for the current GPU processor and - MessageRouter, which will provide future GPU processors. - """ - gpu_language = self.gpu_language_box.currentData() - MessageRouter.get_instance().gpu_language = gpu_language - if self._prev_gpu_proc is not None: - shader_html_data = processor_to_shader_html( - self._prev_gpu_proc, gpu_language - ) - self._on_shader_html_ready(shader_html_data, self._prev_gpu_proc) - - def _on_export_button_released(self) -> None: - """Write the current CTF to disk.""" - if self._prev_group_tf is not None: - ctf_path_str, file_filter = QtWidgets.QFileDialog.getSaveFileName( - self, "Export CTF File", filter="CTF (*.ctf)" - ) - if ctf_path_str: - config = ocio.GetCurrentConfig() - self._prev_group_tf.write( - "Color Transform Format", ctf_path_str, config - ) - - def _on_tab_changed(self, index: int) -> None: - """Only update visible tabs.""" - msg_router = MessageRouter.get_instance() - - if index == -1: - msg_router.config_updates_allowed = False - msg_router.ctf_updates_allowed = False - msg_router.shader_updates_allowed = False - return - - widget = self.tabs.widget(index) - - if widget == self.config_view: - msg_router.config_updates_allowed = True - msg_router.ctf_updates_allowed = False - msg_router.shader_updates_allowed = False - elif widget == self.ctf_view: - msg_router.config_updates_allowed = False - msg_router.ctf_updates_allowed = True - msg_router.shader_updates_allowed = False - elif widget == self.shader_view: - msg_router.config_updates_allowed = False - msg_router.ctf_updates_allowed = False - msg_router.shader_updates_allowed = True diff --git a/src/apps/ocioview/ocioview/inspect/curve_inspector.py b/src/apps/ocioview/ocioview/inspect/curve_inspector.py deleted file mode 100644 index e5298cc1d4..0000000000 --- a/src/apps/ocioview/ocioview/inspect/curve_inspector.py +++ /dev/null @@ -1,805 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import enum -import math -from typing import Optional - -import numpy as np - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import R_COLOR, G_COLOR, B_COLOR, GRAY_COLOR, ICON_SIZE_TAB -from ..message_router import MessageRouter -from ..processor_context import ProcessorContext -from ..utils import get_glyph_icon, SignalsBlocked -from ..widgets import EnumComboBox, FloatEditArray, IntEdit - - -class SampleType(enum.Enum): - """Enum of curve sampling types for representing OCIO transforms.""" - - LINEAR = "linear" - """Linear scale.""" - - LOG = "log" - """Log scale with a user-defined base.""" - - -class CurveInspector(QtWidgets.QWidget): - """ - Widget for inspecting OCIO transform tone curves, which updates - asynchronously when visible, to reduce unnecessary background - processing. - """ - - @classmethod - def label(cls) -> str: - return "Curve" - - @classmethod - def icon(cls) -> QtGui.QIcon: - 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) - - # Widgets - self.input_range_label = get_glyph_icon("mdi6.import", as_widget=True) - self.input_range_label.setToolTip("Input range") - self.input_range_edit = FloatEditArray( - labels=["min", "max"], - 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.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.setToolTip(self.sample_count_label.toolTip()) - 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.setToolTip("Sample type") - self.sample_type_combo = EnumComboBox(SampleType) - self.sample_type_combo.setToolTip(self.sample_type_label.toolTip()) - self.sample_type_combo.currentIndexChanged[int].connect( - self._on_sample_type_changed - ) - - self.log_base_label = get_glyph_icon("mdi6.math-log", as_widget=True) - self.log_base_label.setToolTip("Log base") - self.log_base_edit = IntEdit(default=CurveView.LOG_BASE_DEFAULT) - self.log_base_edit.setToolTip(self.log_base_label.toolTip()) - self.log_base_edit.value_changed.connect(self._on_log_base_changed) - self.log_base_edit.setEnabled(False) - - self.view = CurveView() - - # Layout - option_layout = QtWidgets.QHBoxLayout() - option_layout.addWidget(self.input_range_label) - option_layout.addWidget(self.input_range_edit) - option_layout.setStretch(1, 3) - option_layout.addWidget(self.sample_count_label) - option_layout.addWidget(self.sample_count_edit) - option_layout.setStretch(3, 2) - option_layout.addWidget(self.sample_type_label) - option_layout.addWidget(self.sample_type_combo) - option_layout.setStretch(5, 3) - option_layout.addWidget(self.log_base_label) - option_layout.addWidget(self.log_base_edit) - option_layout.setStretch(7, 2) - - layout = QtWidgets.QVBoxLayout() - layout.addLayout(option_layout) - layout.addWidget(self.view) - - self.setLayout(layout) - - def reset(self) -> None: - """Clear rendered curves.""" - self.view.reset() - - @QtCore.Slot(str, float) - def _on_input_range_changed(self, label: str, value: float) -> None: - """ - Triggered when the user changes the input range, which defines - the input values to be sampled through the transform. - - :param label: Float edit label - :param value: Float edit value - """ - self.view.set_input_range( - self.input_range_edit.component_value("min"), - self.input_range_edit.component_value("max"), - ) - - @QtCore.Slot(int) - def _on_sample_count_changed(self, sample_count: int) -> None: - """ - Triggered when the user changes the number of samples to be - processed within the input range through the transform. - - :param sample_count: Number of samples. Typically, a power of 2 - number. - """ - if sample_count >= CurveView.SAMPLE_COUNT_MIN: - self.view.set_sample_count(sample_count) - else: - with SignalsBlocked(self.sample_count_edit): - self.sample_count_edit.set_value(CurveView.SAMPLE_COUNT_MIN) - self.view.set_sample_count(CurveView.SAMPLE_COUNT_MIN) - - @QtCore.Slot(int) - def _on_sample_type_changed(self, index: int) -> None: - """ - Triggered when the user changes the sample type, which defines - how samples are distributed within the input range. - - :param index: Curve type index - """ - sample_type = self.sample_type_combo.member() - self.log_base_edit.setEnabled(sample_type == SampleType.LOG) - self.view.set_sample_type(sample_type) - - @QtCore.Slot(int) - def _on_log_base_changed(self, log_base: int) -> None: - """ - Triggered when the user changes the base for the log sample - type. - - :param log_base: Log scale base - """ - self.view.set_log_base(log_base) - - -class CurveView(QtWidgets.QGraphicsView): - """ - Widget for rendering OCIO transform tone curves. - """ - - SAMPLE_COUNT_MIN = 2**8 - SAMPLE_COUNT_DEFAULT = 2**10 - INPUT_MIN_DEFAULT = 0.0 - INPUT_MAX_DEFAULT = 1.0 - LOG_BASE_DEFAULT = 2 - CURVE_SCALE = 100 - FONT_HEIGHT = 4 - - # The curve viewer only shows 5 digit decimal precision, so this should work fine - # as a minimum when input min is 0. - EPSILON = 1e-5 - - def __init__( - self, - input_min: float = INPUT_MIN_DEFAULT, - input_max: float = INPUT_MAX_DEFAULT, - sample_count: int = SAMPLE_COUNT_DEFAULT, - sample_type: SampleType = SampleType.LINEAR, - log_base: int = LOG_BASE_DEFAULT, - parent: Optional[QtWidgets.QWidget] = None, - ): - """ - :param input_min: Input range minimum value - :param input_max: Input range maximum value - :param sample_count: Number of samples in the input range, - which will typically be a power of 2 value. - :param sample_type: Sample scale/distribution type - :param log_base: Log scale base when sample_type is - `SampleType.LOG`. - """ - super().__init__(parent=parent) - self.setRenderHint(QtGui.QPainter.Antialiasing, True) - self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setMouseTracking(True) - self.scale(1, -1) # Flip to make origin the bottom-left corner - - # Input range - if input_max <= input_min: - input_min = self.INPUT_MIN_DEFAULT - input_max = self.INPUT_MAX_DEFAULT - - self._input_min = input_min - self._input_max = input_max - - # Sample characteristics - self._sample_count = max(self.SAMPLE_COUNT_MIN, sample_count) - self._sample_type = sample_type - self._log_base = max(2, log_base) - - # Cached sample data - self._sample_ellipse: Optional[QtWidgets.QGraphicsEllipseItem] = None - self._sample_text: Optional[QtWidgets.QGraphicsTextItem] = None - self._sample_rect: Optional[QtCore.QRectF] = None - - self._samples: dict[str, np.ndarray] = {} - self._nearest_samples: dict[str, tuple[float, float, float]] = {} - - self._x_lin: np.ndarray = np.array([], dtype=np.float32) - self._x_log: np.ndarray = np.array([], dtype=np.float32) - self._x_min: float = self._input_min - self._x_max: float = self._input_max - self._y_min: float = self._input_min - self._y_max: float = self._input_max - - # Cached curve data - self._curve_init = False - self._curve_tf = QtGui.QTransform() - self._curve_tf_inv = QtGui.QTransform() - self._curve_paths: dict[str, QtGui.QPainterPath] = {} - self._curve_items: dict[str, QtWidgets.QGraphicsPathItem] = {} - self._curve_rect = QtCore.QRectF( - self._input_min, - self._input_min, - self._input_max - self._input_min, - self._input_max - self._input_min, - ) - - # Cached processor from which the OCIO transform is derived - self._prev_proc_context = None - self._prev_cpu_proc = None - - # Graphics scene - self._scene = QtWidgets.QGraphicsScene() - self.setScene(self._scene) - - # Initialize - self._update_x_samples() - msg_router = MessageRouter.get_instance() - msg_router.processor_ready.connect(self._on_processor_ready) - - def showEvent(self, event: QtGui.QShowEvent) -> None: - """Start listening for processor updates, if visible.""" - super().showEvent(event) - - msg_router = MessageRouter.get_instance() - msg_router.processor_updates_allowed = True - - def hideEvent(self, event: QtGui.QHideEvent) -> None: - """Stop listening for processor updates, if not visible.""" - super().hideEvent(event) - - msg_router = MessageRouter.get_instance() - msg_router.processor_updates_allowed = False - - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: - """Re-fit graph on resize, to always be centered.""" - super().resizeEvent(event) - self._fit() - - def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: - """Find nearest samples to current mouse position.""" - super().mouseMoveEvent(event) - - if not self._curve_init: - return - - self._nearest_samples.clear() - - pos = self._curve_tf_inv.map(self.mapToScene(event.pos())) - pos_arr = np.array([pos.x(), pos.y()], dtype=np.float32) - - for color_name, samples in self._samples.items(): - all_dist = np.linalg.norm(samples - pos_arr, axis=1) - nearest_dist_index = np.argmin(all_dist) - self._nearest_samples[color_name] = ( - ( - self._x_lin[nearest_dist_index] - if self._sample_type == SampleType.LINEAR - else self._x_log[nearest_dist_index] - ), - samples[nearest_dist_index][0], - samples[nearest_dist_index][1], - ) - - self._invalidate() - - def wheelEvent(self, event: QtGui.QWheelEvent) -> None: - """ - Ignore wheel events to prevent scrolling around graphics scene. - """ - event.ignore() - - def reset(self) -> None: - """Clear all curves.""" - self._samples.clear() - self._nearest_samples.clear() - - self._curve_init = False - self._curve_paths.clear() - self._curve_items.clear() - - self._prev_cpu_proc = None - - self._scene.clear() - self._invalidate() - - def set_input_range(self, input_min: float, input_max: float) -> None: - """ - Set the input range, which defines the input values to be - sampled through the transform. - - :param input_min: Input range minimum value - :param input_max: Input range maximum value - """ - if ( - input_min != self._input_min or input_max != self._input_max - ) and input_max > input_min: - self._input_min = input_min - self._input_max = input_max - self._update_curves() - - def set_input_min(self, input_min: float) -> None: - """ - Set the input minimum, which defines the lowest value to be - sampled through the transform. - - :param input_min: Input range minimum value - """ - if input_min != self._input_min and input_min < self._input_max: - self._input_min = input_min - self._update_curves() - - def set_input_max(self, input_max: float) -> None: - """ - Set the input maximum, which defines the highest value to be - sampled through the transform. - - :param input_max: Input range minimum value - """ - if input_max != self._input_max and input_max > self._input_min: - self._input_max = input_max - self._update_curves() - - def set_sample_count(self, sample_count: int) -> None: - """ - Set the number of samples to be processed within the input - range through the transform. - - :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 - ): - self._sample_count = sample_count - self._update_curves() - - def set_sample_type(self, sample_type: SampleType) -> None: - """ - Set the sample type, which defines how samples are distributed - within the input range. - - :param sample_type: Curve type index - """ - if sample_type != self._sample_type: - self._sample_type = sample_type - self._update_curves() - - def set_log_base(self, log_base: int) -> None: - """ - Set the base for the log sample type. - - :param log_base: Log scale base - """ - if log_base != self._log_base and log_base >= 2: - self._log_base = log_base - self._update_curves() - - def drawBackground( - self, painter: QtGui.QPainter, rect: QtCore.QRectF - ) -> None: - """Draw curve grid and axis values.""" - # Flood fill background - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtCore.Qt.black) - painter.drawRect(rect) - - if not self._curve_init: - return - - font = painter.font() - font.setPixelSize(self.FONT_HEIGHT) - painter.setFont(font) - - text_pen = QtGui.QPen(GRAY_COLOR) - grid_pen = QtGui.QPen(GRAY_COLOR.darker(200)) - grid_pen.setWidthF(0) - - painter.setPen(grid_pen) - painter.setBrush(QtCore.Qt.NoBrush) - - # Draw grid border - painter.drawRect(self._curve_rect) - - # Draw grid rows - y_text_origin = QtGui.QTextOption( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter - ) - y_text_origin.setWrapMode(QtGui.QTextOption.NoWrap) - - for i, y in enumerate( - np.linspace(self._y_min, self._y_max, 11, dtype=np.float32) - ): - p1 = self._curve_tf.map(QtCore.QPointF(self._x_min, y)) - p2 = self._curve_tf.map(QtCore.QPointF(self._x_max, y)) - - if self._y_min < y < self._y_max: - painter.setPen(grid_pen) - painter.drawLine(QtCore.QLineF(p1, p2)) - - if y > self._y_min: - label_value = round(y, 2) - if label_value == 0.0: - label_value = abs(label_value) - - painter.save() - painter.translate(p1) - painter.scale(1, -1) - painter.setPen(text_pen) - painter.drawText( - 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.setWrapMode(QtGui.QTextOption.NoWrap) - - sample_step = math.ceil(self._sample_count / 10.0) - - for i, x in enumerate(self._x_lin): - if not (i % sample_step == 0 or i == self._sample_count - 1): - continue - - p1 = self._curve_tf.map(QtCore.QPointF(x, self._y_min)) - p2 = self._curve_tf.map(QtCore.QPointF(x, self._y_max)) - - if self._x_min < x < self._x_max: - painter.setPen(grid_pen) - painter.drawLine(QtCore.QLineF(p1, p2)) - - if x > self._x_min: - label_value = round( - 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: - label_value = abs(label_value) - - painter.save() - painter.translate(p1) - painter.scale(1, -1) - painter.rotate(90) - painter.setPen(text_pen) - painter.drawText( - 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: - """Draw nearest sample point and coordinates.""" - if not self._curve_init: - return - - font = painter.font() - font.setPixelSize(self.FONT_HEIGHT) - painter.setFont(font) - - text_origin = QtGui.QTextOption( - QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter - ) - text_origin.setWrapMode(QtGui.QTextOption.NoWrap) - - sample_l = sample_t = None - if self._sample_rect is not None: - sample_l = self._sample_rect.left() - sample_t = self._sample_rect.top() - - for i, (color_name, nearest_sample) in enumerate( - reversed(self._nearest_samples.items()) - ): - # Draw sample point on curve - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtGui.QColor(color_name)) - - painter.drawEllipse( - self._curve_tf.map( - QtCore.QPointF( - nearest_sample[1], - nearest_sample[2], - ) - ), - 1.5, - 1.5, - ) - - # Draw sample values - if sample_l is not None: - painter.setBrush(QtCore.Qt.NoBrush) - - x_label_value = f"{nearest_sample[0]:.05f}" - y_label_value = f"{nearest_sample[2]:.05f}" - - painter.save() - 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 - ) - - if color_name == GRAY_COLOR.name(): - palette = self.palette() - painter.setPen(palette.color(palette.ColorRole.Text)) - else: - painter.setPen(QtGui.QColor(color_name)) - - painter.drawText( - QtCore.QRectF(5, -20, 35, 10), x_label_value, text_origin - ) - painter.drawText( - QtCore.QRectF(5, -10, 35, 10), y_label_value, text_origin - ) - painter.restore() - - def _invalidate(self) -> None: - """Force repaint of visible region of graphics scene.""" - self._scene.invalidate( - QtCore.QRectF(self.visibleRegion().boundingRect()) - ) - - def _update_curves(self) -> None: - """ - Update X-axis samples and rebuild curves from any cached - processor. - """ - self._update_x_samples() - 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): - """ - Update linear and log X-axis samples from input and sample - parameters. - """ - self._x_lin = np.linspace( - 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) - ) - self._x_log = np.logspace( - log_min, - log_max, - self._sample_count, - base=self._log_base, - dtype=np.float32, - ) - - self._x_min = self._x_lin.min() - self._x_max = self._x_lin.max() - - def _fit(self) -> None: - """Fit and center graph.""" - if not self._curve_init: - return - - # Use font metrics to calculate text padding, based on estimated maximum - # number lengths. - font = self.font() - font.setPixelSize(self.FONT_HEIGHT) - fm = QtGui.QFontMetrics(font) - - text_rect = QtCore.QRect(0, 0, 100, 10) - text_flags = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter - - pad_b = 15 - pad_t = ( - fm.boundingRect( - text_rect, - text_flags, - "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 - ) - - fit_rect = self._curve_rect.adjusted(-pad_l, -pad_t, pad_r, pad_b) - - # Fit and center on calculated rectangle - self.fitInView(fit_rect, QtCore.Qt.KeepAspectRatio) - self.centerOn(fit_rect.center()) - self.update() - - @QtCore.Slot(ocio.CPUProcessor) - def _on_processor_ready( - self, proc_context: ProcessorContext, cpu_proc: ocio.CPUProcessor - ) -> None: - """ - Update curves from sampled OCIO CPU processor. - - :param proc_context: OCIO processor context data - :param cpu_proc: CPU processor of currently viewed transform - """ - self.reset() - - self._prev_proc_context = proc_context - self._prev_cpu_proc = cpu_proc - - # Get input samples - if self._sample_type == SampleType.LOG: - x_samples = self._x_log - else: # LINEAR - x_samples = self._x_lin - - # Interleave samples per channel - rgb_samples = np.repeat(x_samples, 3) - - # Apply processor to samples - cpu_proc.applyRGB(rgb_samples) - - # De-interleave transformed samples - r_samples = rgb_samples[0::3] - g_samples = rgb_samples[1::3] - 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): - 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._y_min = r_samples.min() - self._y_max = r_samples.max() - - else: - y_min = None - y_max = None - - for i, (color, channel_samples) in enumerate( - [ - (R_COLOR, r_samples), - (G_COLOR, g_samples), - (B_COLOR, b_samples), - ] - ): - color_name = color.name() - - self._samples[color_name] = np.stack( - (self._x_lin, channel_samples), axis=-1 - ) - - channel_y_min = channel_samples.min() - if y_min is None or channel_y_min < y_min: - y_min = channel_y_min - channel_y_max = channel_samples.max() - if y_max is None or channel_y_max > y_max: - y_max = channel_y_max - - self._y_min = y_min - self._y_max = y_max - - # Transform to scale/translate curve so that it fits in a square and - # has its origin at (0, 0). - curve_tf = QtGui.QTransform() - curve_tf.translate(-self._x_min, -self._y_min) - curve_tf.scale( - self.CURVE_SCALE / (self._x_max - self._x_min), - self.CURVE_SCALE / (self._y_max - self._y_min), - ) - self._curve_tf = curve_tf - self._curve_tf_inv, ok = curve_tf.inverted() - - # Curve square - self._curve_rect = QtCore.QRectF( - self._curve_tf.map(QtCore.QPointF(self._x_min, self._y_min)), - self._curve_tf.map(QtCore.QPointF(self._x_max, self._y_max)), - ) - - # Sample box rect - self._sample_rect = QtCore.QRectF( - self._curve_rect.right() + 5, - self._curve_rect.top(), - 40, - len(self._samples) * 20, - ) - - # Build painter paths that fit in square from sample data - 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] - ) - ) - ) - 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] - ) - ) - ) - self._curve_paths[color_name] = curve - - # Add curve(s) to scene - for color_name, curve in self._curve_paths.items(): - pen = QtGui.QPen() - pen.setColor(QtGui.QColor(color_name)) - pen.setWidthF(0.5) - - curve_item = self._scene.addPath( - curve, pen, QtGui.QBrush(QtCore.Qt.NoBrush) - ) - self._curve_items[color_name] = curve_item - - self._curve_init = True - - # 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 - ) - 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 deleted file mode 100644 index 8675c24174..0000000000 --- a/src/apps/ocioview/ocioview/inspect/log_inspector.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_TAB -from ..log_handlers import set_logging_level -from ..message_router import MessageRouter -from ..utils import get_glyph_icon -from ..widgets import ComboBox, LogView - - -class LogInspector(QtWidgets.QWidget): - """ - Widget for inspecting OCIO and application logs. - """ - - @classmethod - def label(cls) -> str: - return "Log" - - @classmethod - def icon(cls) -> QtGui.QIcon: - return get_glyph_icon("ph.terminal-window", size=ICON_SIZE_TAB) - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # 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.addItem("Info", userData=ocio.LOGGING_LEVEL_INFO) - self.log_level_box.addItem("Debug", userData=ocio.LOGGING_LEVEL_DEBUG) - self.log_level_box.setCurrentText( - ocio.LoggingLevelToString(ocio.GetLoggingLevel()).capitalize() - ) - - self.clear_button = QtWidgets.QToolButton() - self.clear_button.setIcon(get_glyph_icon("mdi6.delete-outline")) - - self.log_view = LogView() - self.log_view.prepend_tool_bar_widget(self.log_level_box) - self.log_view.append_tool_bar_widget(self.clear_button) - - # Layout - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab(self.log_view, self.icon(), self.label()) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.tabs) - self.setLayout(layout) - - # Initialize - self.log_level_box.currentIndexChanged[int].connect( - self._on_log_level_changed - ) - self.clear_button.released.connect(self.reset) - - log_router = MessageRouter.get_instance() - log_router.error_logged.connect(self._on_record_logged) - log_router.warning_logged.connect(self._on_record_logged) - log_router.info_logged.connect(self._on_record_logged) - log_router.debug_logged.connect(self._on_record_logged) - - def reset(self) -> None: - """Clear log history.""" - self.log_view.reset() - - @QtCore.Slot(int) - def _on_log_level_changed(self, index: int): - """Update global logging level.""" - log_level = self.log_level_box.currentData() - set_logging_level(log_level) - - @QtCore.Slot(str) - def _on_record_logged(self, record: str) -> None: - """Append record to general log view.""" - self.log_view.append(record) diff --git a/src/apps/ocioview/ocioview/inspect_dock.py b/src/apps/ocioview/ocioview/inspect_dock.py deleted file mode 100644 index 99fcab7f2a..0000000000 --- a/src/apps/ocioview/ocioview/inspect_dock.py +++ /dev/null @@ -1,77 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtWidgets - -from .inspect import ( - ChromaticitiesInspector, - CodeInspector, - CurveInspector, - LogInspector, -) -from .utils import get_glyph_icon -from .widgets.structure import TabbedDockWidget - - -class InspectDock(TabbedDockWidget): - """ - Dockable widget for inspecting and visualizing config and color - transform data. - """ - - 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 - ) - self.tabs.setTabPosition(QtWidgets.QTabWidget.West) - - # Widgets - self.chromaticities_inspector = ChromaticitiesInspector() - self.curve_inspector = CurveInspector() - self.code_inspector = CodeInspector() - self.log_inspector = LogInspector() - - # Layout - self.add_tab( - self.chromaticities_inspector, - self.chromaticities_inspector.label(), - self.chromaticities_inspector.icon(), - ) - self.add_tab( - self.curve_inspector, - self.curve_inspector.label(), - self.curve_inspector.icon(), - ) - self.add_tab( - self.code_inspector, - self.code_inspector.label(), - self.code_inspector.icon(), - ) - self.add_tab( - self.log_inspector, - self.log_inspector.label(), - self.log_inspector.icon(), - ) - - def reset(self) -> None: - """Reset data for all inspectors, except the log.""" - self.chromaticities_inspector.reset() - self.curve_inspector.reset() - self.code_inspector.reset() diff --git a/src/apps/ocioview/ocioview/items/__init__.py b/src/apps/ocioview/ocioview/items/__init__.py deleted file mode 100644 index 56f03397af..0000000000 --- a/src/apps/ocioview/ocioview/items/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from .color_space_edit import ColorSpaceEdit -from .config_properties_edit import ConfigPropertiesEdit -from .display_view_edit import DisplayViewEdit -from .look_edit import LookEdit -from .named_transform_edit import NamedTransformEdit -from .role_edit import RoleEdit -from .rule_edit import RuleEdit -from .view_transform_edit import ViewTransformEdit diff --git a/src/apps/ocioview/ocioview/items/active_display_view_edit.py b/src/apps/ocioview/ocioview/items/active_display_view_edit.py deleted file mode 100644 index a13ea05f39..0000000000 --- a/src/apps/ocioview/ocioview/items/active_display_view_edit.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_ITEM -from ..widgets import ItemModelListWidget -from ..utils import get_glyph_icon -from .active_display_view_model import ActiveDisplayModel, ActiveViewModel - - -class ActiveDisplayEdit(ItemModelListWidget): - """Widget for editing active displays in the current config.""" - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - self.model = ActiveDisplayModel() - - super().__init__( - self.model, - self.model.NAME.column, - item_icon=self.model.item_type_icon(), - items_constant=True, - parent=parent, - ) - - self.model.item_selection_requested.connect( - lambda index: self.set_current_row(index.row()) - ) - - -class ActiveViewEdit(ItemModelListWidget): - """Widget for editing active views in the current config.""" - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - self.model = ActiveViewModel() - - super().__init__( - self.model, - self.model.NAME.column, - item_icon=self.model.item_type_icon(), - items_constant=True, - parent=parent, - ) - - self.model.item_selection_requested.connect( - lambda index: self.set_current_row(index.row()) - ) - - -class ActiveDisplayViewEdit(QtWidgets.QWidget): - """ - Widget for editing the active display and view lists for the - current config. - - .. note:: - The active display and view edits control display and view - visibility and order in an application's UI. - """ - - @classmethod - def item_type_icon(cls) -> QtGui.QIcon: - return get_glyph_icon( - "mdi6.sort-bool-ascending-variant", size=ICON_SIZE_ITEM - ) - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - return f"Active Display{'s' if plural else ''} and View{'s' if plural else ''}" - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.active_display_edit = ActiveDisplayEdit() - self.active_view_edit = ActiveViewEdit() - - # Layout - self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) - self.splitter.setOpaqueResize(False) - self.splitter.addWidget(self.active_display_edit) - self.splitter.addWidget(self.active_view_edit) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.splitter) - self.setLayout(layout) diff --git a/src/apps/ocioview/ocioview/items/active_display_view_model.py b/src/apps/ocioview/ocioview/items/active_display_view_model.py deleted file mode 100644 index 47c4db06d0..0000000000 --- a/src/apps/ocioview/ocioview/items/active_display_view_model.py +++ /dev/null @@ -1,213 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from dataclasses import dataclass -from typing import Any, Callable, Optional, Type - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..undo import ConfigSnapshotUndoCommand -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -@dataclass -class Display: - """Individual display storage.""" - - name: str - active: False - - -@dataclass -class View: - """Individual view storage.""" - - name: str - active: False - - -class BaseActiveDisplayViewModel(BaseConfigItemModel): - """ - Base item model for active displays and views in the current - config. - """ - - ACTIVE = ColumnDesc(1, "Active", bool) - - # OCIO config object type this model manages. - __item_type__: type = None - - # Callable to get all items from the config cache - __get_all_items__: Callable = None - - # Callable to get active items from the config cache - __get_active_items__: Callable = None - - # Config attribute name for method to set the active item string - __set_active_items_attr__: str = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - ConfigCache.register_reset_callback(self._reset_cache) - - def move_item_up(self, item_name: str) -> bool: - active_names = self.__get_active_items__() - if item_name not in active_names: - return False - - src_row = active_names.index(item_name) - dst_row = max(0, src_row - 1) - - if dst_row == src_row: - return False - - 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__() - if item_name not in active_names: - return False - - src_row = active_names.index(item_name) - dst_row = min(len(active_names) - 1, src_row + 1) - - if dst_row == src_row: - return False - - 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 - - def get_item_names(self) -> list[str]: - return [item.name for item in self._get_items()] - - def _get_undo_command_type( - self, column_desc: ColumnDesc - ) -> Type[QtGui.QUndoCommand]: - if column_desc == self.ACTIVE: - # Changing check state of the ACTIVE column has side effects related to - # display/view order, so a config snapshot is needed to revert the change. - return ConfigSnapshotUndoCommand - else: - return super()._get_undo_command_type(column_desc) - - def _get_icon( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - if column_desc == self.NAME: - return self.item_type_icon() - else: - return None - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[__item_type__]: - if ConfigCache.validate() and self._items: - return self._items - - all_names = self.__get_all_items__() - active_names = self.__get_active_items__() - - self._items = [] - - for name in active_names: - self._items.append(self.__item_type__(name, True)) - for name in all_names: - if name not in active_names: - self._items.append(self.__item_type__(name, False)) - - return self._items - - def _clear_items(self) -> None: - getattr(ocio.GetCurrentConfig(), self.__set_active_items_attr__)("") - - def _add_item(self, item: __item_type__) -> None: - active_names = self.__get_active_items__() - if item.active and item.name not in active_names: - active_names.append(item.name) - getattr(ocio.GetCurrentConfig(), self.__set_active_items_attr__)( - ",".join(active_names) - ) - - def _remove_item(self, item: __item_type__) -> None: - active_names = self.__get_active_items__() - if not item.active and item.name in active_names: - active_names.remove(item.name) - getattr(ocio.GetCurrentConfig(), self.__set_active_items_attr__)( - ",".join(active_names) - ) - - def _new_item(self, name: __item_type__) -> None: - # Existing config items only - pass - - def _get_checked_column(self) -> Optional[ColumnDesc]: - return self.ACTIVE - - def _get_value(self, item: __item_type__, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.name - elif column_desc == self.ACTIVE: - return item.active - - # Invalid column - return None - - def _set_value( - self, - item: __item_type__, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - # Update parameters - if column_desc == self.ACTIVE: - item.active = value - if value is True: - self._add_item(item) - self.item_added.emit(item.name) - else: - self._remove_item(item) - self.item_removed.emit() - - -class ActiveDisplayModel(BaseActiveDisplayViewModel): - """ - Item model for active displays in the current config. - """ - - NAME = ColumnDesc(0, "Display", str) - - COLUMNS = [NAME, BaseActiveDisplayViewModel.ACTIVE] - - __item_type__ = Display - __icon_glyph__ = "mdi6.monitor" - __get_active_items__ = ConfigCache.get_active_displays - __get_all_items__ = ConfigCache.get_displays - __set_active_items_attr__ = "setActiveDisplays" - - -class ActiveViewModel(BaseActiveDisplayViewModel): - """ - Item model for active views in the current config. - """ - - NAME = ColumnDesc(0, "View", str) - - COLUMNS = [NAME, BaseActiveDisplayViewModel.ACTIVE] - - __item_type__ = View - __icon_glyph__ = "mdi6.eye-outline" - __get_active_items__ = ConfigCache.get_active_views - __get_all_items__ = ConfigCache.get_views - __set_active_items_attr__ = "setActiveViews" diff --git a/src/apps/ocioview/ocioview/items/color_space_edit.py b/src/apps/ocioview/ocioview/items/color_space_edit.py deleted file mode 100644 index 8b451aa146..0000000000 --- a/src/apps/ocioview/ocioview/items/color_space_edit.py +++ /dev/null @@ -1,221 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -from PySide6 import QtCore, QtWidgets -import PyOpenColorIO as ocio - -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, - EnumComboBox, - CallbackComboBox, - FloatEditArray, - StringListWidget, - TextEdit, -) -from .color_space_model import ColorSpaceModel -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit - - -class ColorSpaceParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing the parameters and transforms for one color - space. - """ - - __model_type__ = ColorSpaceModel - __has_transforms__ = True - __from_ref_column_desc__ = ColorSpaceModel.FROM_REFERENCE - __to_ref_column_desc__ = ColorSpaceModel.TO_REFERENCE - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.reference_space_type_combo = EnumComboBox( - ocio.ReferenceSpaceType, - icons={ - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon( - "ph.sun", size=ICON_SIZE_ITEM - ), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( - "ph.monitor", size=ICON_SIZE_ITEM - ), - }, - ) - self.aliases_list = StringListWidget( - item_basename="alias", - item_icon=get_glyph_icon( - "ph.bookmark-simple", size=ICON_SIZE_ITEM - ), - ) - 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 - ) - self.description_edit = TextEdit() - 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_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 - ), - get_presets=self._get_available_categories, - ) - - # Layout - self._param_layout.addRow( - 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.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.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_VARS.label, self.allocation_vars_edit - ) - self._param_layout.addRow( - self.model.CATEGORIES.label, self.categories_list - ) - - def update_available_allocation_vars(self) -> None: - """ - Enable the interface needed to edit this color space's allocation - vars for the current allocation type. - """ - allocation = self.allocation_combo.member() - - for i in range(2): - self.allocation_vars_edit.value_edits[i].setEnabled( - allocation != ocio.ALLOCATION_UNKNOWN - ) - self.allocation_vars_edit.value_edits[2].setEnabled( - allocation == ocio.ALLOCATION_LG2 - ) - - @QtCore.Slot(int) - def _on_allocation_changed(self, index: int) -> None: - self.update_available_allocation_vars() - - def _get_available_categories(self) -> list[str]: - """ - :return: All unused categories which can be added as presets - to this item. - """ - current_categories = self.categories_list.items() - return [ - c - for c in ConfigCache.get_categories() - if c not in current_categories - ] - - -class ColorSpaceEdit(BaseConfigItemEdit): - """ - Widget for editing all color spaces in the current config. - """ - - __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) - - model = self.model - - # Map widgets to model columns - 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.param_edit.equality_group_edit, model.EQUALITY_GROUP.column - ) - 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.param_edit.allocation_combo, model.ALLOCATION.column - ) - self.mapper.addMapping( - self.param_edit.allocation_vars_edit, model.ALLOCATION_VARS.column - ) - 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 - ) - - # 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) - ) - self.param_edit.is_data_check.stateChanged.connect( - partial(self.param_edit.submit_mapper_deferred, self.mapper) - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) - - @QtCore.Slot(int) - def _on_current_row_changed(self, row: int) -> None: - super()._on_current_row_changed(row) - - if row != -1: - # Update allocation var widget states, since allocation may differ from - # the previous color space. - self.param_edit.update_available_allocation_vars() diff --git a/src/apps/ocioview/ocioview/items/color_space_model.py b/src/apps/ocioview/ocioview/items/color_space_model.py deleted file mode 100644 index bc65316499..0000000000 --- a/src/apps/ocioview/ocioview/items/color_space_model.py +++ /dev/null @@ -1,295 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import copy -from typing import Any, Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..ref_space_manager import ReferenceSpaceManager -from ..utils import get_enum_member, get_glyph_icon -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -class ColorSpaceModel(BaseConfigItemModel): - """ - Item model for editing color spaces in the current config. - """ - - REFERENCE_SPACE_TYPE = ColumnDesc(0, "Reference Space Type", int) - NAME = ColumnDesc(1, "Name", str) - ALIASES = ColumnDesc(2, "Aliases", list) - FAMILY = ColumnDesc(3, "Family", str) - ENCODING = ColumnDesc(4, "Encoding", str) - EQUALITY_GROUP = ColumnDesc(5, "Equality Group", str) - DESCRIPTION = ColumnDesc(6, "Description", str) - BIT_DEPTH = ColumnDesc(7, "Bit-Depth", int) - IS_DATA = ColumnDesc(8, "Is Data", bool) - ALLOCATION = ColumnDesc(9, "Allocation", int) - ALLOCATION_VARS = ColumnDesc(10, "Allocation Vars", list) - CATEGORIES = ColumnDesc(11, "Categories", list) - TO_REFERENCE = ColumnDesc(12, "To Reference", ocio.Transform) - FROM_REFERENCE = ColumnDesc(13, "From Reference", ocio.Transform) - - # fmt: off - COLUMNS = sorted([ - REFERENCE_SPACE_TYPE, NAME, ALIASES, FAMILY, ENCODING, EQUALITY_GROUP, - DESCRIPTION, BIT_DEPTH, IS_DATA, ALLOCATION, ALLOCATION_VARS, CATEGORIES, - TO_REFERENCE, FROM_REFERENCE, - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = ocio.ColorSpace - __icon_glyph__ = "ph.swap" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - self._items = ocio.ColorSpaceSet() - - self._ref_space_icons = { - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon( - "ph.sun", size=ICON_SIZE_ITEM - ), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( - "ph.monitor", size=ICON_SIZE_ITEM - ), - } - - # Update on external config changes, in this case when a required reference - # space is created. - ReferenceSpaceManager.subscribe_to_reference_spaces(self.repaint) - - def get_item_names(self) -> list[str]: - return [item.getName() for item in self._get_items()] - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get color space name from subscription item label - item_name = self.extract_subscription_item_name(item_label) - - ref_space_name = ( - ReferenceSpaceManager.scene_reference_space().getName() - ) - return ( - ocio.ColorSpaceTransform(src=ref_space_name, dst=item_name), - ocio.ColorSpaceTransform(src=item_name, dst=ref_space_name), - ) - - def _can_item_be_removed(self, item: ocio.ColorSpace) -> tuple[bool, str]: - config = ocio.GetCurrentConfig() - if config.isColorSpaceUsed(item.getName()): - return ( - False, - "is referenced by one or more transforms, roles, or looks in the " - "config.", - ) - else: - return True, "" - - def _get_icon( - self, item: ocio.ColorSpace, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - if column_desc == self.NAME: - return ( - self._get_subscription_icon(item, column_desc) - or self._ref_space_icons[item.getReferenceSpaceType()] - ) - else: - return None - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - if column_desc == self.NAME: - return self._get_subscription_color(item, column_desc) - else: - return None - - def _get_items(self, preserve: bool = False) -> list[ocio.ColorSpace]: - if preserve: - self._items = ConfigCache.get_color_spaces(as_set=True) - return list(self._items.getColorSpaces()) - else: - return ConfigCache.get_color_spaces() - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().clearColorSpaces() - - def _add_item(self, item: ocio.ColorSpace) -> None: - ocio.GetCurrentConfig().addColorSpace(item) - - def _remove_item(self, item: ocio.ColorSpace) -> None: - ocio.GetCurrentConfig().removeColorSpace(item.getName()) - - def _new_item(self, name: str) -> None: - ocio.GetCurrentConfig().addColorSpace( - ocio.ColorSpace( - referenceSpace=ocio.REFERENCE_SPACE_SCENE, name=name - ) - ) - - 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) - elif column_desc == self.NAME: - return item.getName() - elif column_desc == self.ALIASES: - return list(item.getAliases()) - elif column_desc == self.FAMILY: - return item.getFamily() - elif column_desc == self.ENCODING: - return item.getEncoding() - elif column_desc == self.EQUALITY_GROUP: - return item.getEqualityGroup() - elif column_desc == self.DESCRIPTION: - return item.getDescription() - elif column_desc == self.BIT_DEPTH: - return int(item.getBitDepth().value) - elif column_desc == self.IS_DATA: - return item.isData() - elif column_desc == self.ALLOCATION: - return int(item.getAllocation().value) - elif column_desc == self.CATEGORIES: - return list(item.getCategories()) - - # Get allocation variables - elif column_desc == self.ALLOCATION_VARS: - # Make sure there are exactly three values, for compatibility with the - # mapped value edit array. - alloc_vars = item.getAllocationVars() - 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) - ] - elif num_alloc_vars > 3: - alloc_vars = alloc_vars[:3] - return alloc_vars - - # Get transforms - elif column_desc in (self.TO_REFERENCE, self.FROM_REFERENCE): - return item.getTransform( - ocio.COLORSPACE_DIR_TO_REFERENCE - if column_desc == self.TO_REFERENCE - else ocio.COLORSPACE_DIR_FROM_REFERENCE - ) - - # Invalid column - return None - - def _set_value( - self, - item: ocio.ColorSpace, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - config = ocio.GetCurrentConfig() - new_item = None - prev_item_name = item.getName() - - # Changing reference space type requires constructing a new item - if column_desc == self.REFERENCE_SPACE_TYPE: - member = get_enum_member(ocio.ReferenceSpaceType, value) - if member is not None: - new_item = ocio.ColorSpace( - referenceSpace=member, - name=item.getName(), - aliases=list(item.getAliases()), - family=item.getFamily(), - encoding=item.getEncoding(), - equalityGroup=item.getEqualityGroup(), - description=item.getDescription(), - bitDepth=item.getBitDepth(), - 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 - ), - categories=list(item.getCategories()), - ) - - # Otherwise get an editable copy of the current item - if new_item is None: - new_item = copy.deepcopy(item) - - # Update parameters - if column_desc == self.NAME: - if value: - new_item.setName(value) - elif column_desc == self.ALIASES: - new_item.clearAliases() - for alias in value: - new_item.addAlias(alias) - elif column_desc == self.FAMILY: - new_item.setFamily(value) - elif column_desc == self.ENCODING: - new_item.setEncoding(value) - elif column_desc == self.EQUALITY_GROUP: - new_item.setEqualityGroup(value) - elif column_desc == self.DESCRIPTION: - new_item.setDescription(value) - elif column_desc == self.BIT_DEPTH: - member = get_enum_member(ocio.BitDepth, value) - if member is not None: - new_item.setBitDepth(member) - elif column_desc == self.IS_DATA: - new_item.setIsData(value) - elif column_desc == self.ALLOCATION: - member = get_enum_member(ocio.Allocation, value) - if member is not None: - new_item.setAllocation(member) - elif column_desc == self.ALLOCATION_VARS: - new_item.setAllocationVars(value) - elif column_desc == self.CATEGORIES: - new_item.clearCategories() - for category in value: - new_item.addCategory(category) - - # Update transforms - elif column_desc in (self.TO_REFERENCE, self.FROM_REFERENCE): - new_item.setTransform( - value, - ocio.COLORSPACE_DIR_TO_REFERENCE - if column_desc == self.TO_REFERENCE - else ocio.COLORSPACE_DIR_FROM_REFERENCE, - ) - - # Preserve item order when replacing item due to name or reference space - # type change, which requires removing the old item to add the new. - if column_desc in (self.REFERENCE_SPACE_TYPE, self.NAME): - items = ConfigCache.get_color_spaces(as_set=True) - config.clearColorSpaces() - for other_item in items.getColorSpaces(): - if other_item.getName() == prev_item_name: - config.addColorSpace(new_item) - item_name = new_item.getName() - if item_name != prev_item_name: - self.item_renamed.emit(item_name, prev_item_name) - else: - config.addColorSpace(other_item) - - # Item order is preserved for all other changes - else: - config.addColorSpace(new_item) - - # Broadcast transform or name changes to subscribers - 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, - ) diff --git a/src/apps/ocioview/ocioview/items/config_item_edit.py b/src/apps/ocioview/ocioview/items/config_item_edit.py deleted file mode 100644 index 658e1fc45a..0000000000 --- a/src/apps/ocioview/ocioview/items/config_item_edit.py +++ /dev/null @@ -1,460 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -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 -from ..widgets import FormLayout, LineEdit, ItemModelListWidget -from .config_item_model import ColumnDesc, BaseConfigItemModel -from .utils import adapt_splitter_sizes - - -class BaseConfigItemParamEdit(QtWidgets.QWidget): - """ - Widget for editing the parameters and transforms for one config - item model row. - """ - - # Config item model - __model_type__: type = None - - # Whether the config item has an associated pair of transform stacks - __has_transforms__: bool = False - - # Whether to wrap parameters in a tab. This is always True if __has_transforms__ - # is True. - __has_tabs__: bool = False - - # Column descriptor for the "from_reference" equivalent transform model column - __from_ref_column_desc__: ColumnDesc = None - - # Column descriptor for the "to_reference" equivalent transform model column - __to_ref_column_desc__: ColumnDesc = None - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - self.model = self.__model_type__() - self.model.modelAboutToBeReset.connect(self.reset) - - palette = self.palette() - - if self.__has_transforms__: - self.__has_tabs__ = True - 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._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._no_to_ref_icon = get_glyph_icon( - "mdi6.layers-minus", color=no_tf_color, size=ICON_SIZE_TAB - ) - - # Widgets - self.name_edit = LineEdit() - - if self.__has_transforms__: - self.from_ref_stack = TransformEditStack() - self.from_ref_stack.edited.connect( - partial(self._on_transform_edited, self.from_ref_stack) - ) - self.to_ref_stack = TransformEditStack() - self.to_ref_stack.edited.connect( - partial(self._on_transform_edited, self.to_ref_stack) - ) - - # Layout - self._param_layout = FormLayout() - self._param_layout.addRow(self.model.NAME.label, self.name_edit) - - param_spacer_layout = QtWidgets.QVBoxLayout() - param_spacer_layout.addLayout(self._param_layout) - param_spacer_layout.addStretch() - - param_frame = QtWidgets.QFrame() - 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.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding - ) - param_scroll_area.setWidgetResizable(True) - if self.__has_tabs__: - param_scroll_area.setStyleSheet( - f"QScrollArea#config_item_param_edit__param_scroll_area {{" - f" border: none;" - f" border-top: 1px solid " - f" {palette.color(QtGui.QPalette.Dark).name()};" - f" margin-top: {MARGIN_WIDTH:d}px;" - f"}}" - ) - param_scroll_area.setWidget(param_frame) - - if self.__has_tabs__: - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab( - param_scroll_area, - self.model.item_type_icon(), - self.model.item_type_label(), - ) - if self.__has_transforms__: - self.tabs.addTab( - self.from_ref_stack, - self._no_from_ref_icon, - self.__from_ref_column_desc__.label, - ) - self.tabs.addTab( - self.to_ref_stack, - self._no_to_ref_icon, - self.__to_ref_column_desc__.label, - ) - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - if self.__has_tabs__: - layout.addWidget(self.tabs) - else: - layout.addWidget(param_scroll_area) - self.setLayout(layout) - - def reset(self) -> None: - """ - Reset all parameter widgets, for cases where there are no rows - in model. - """ - if self.__has_transforms__: - self.from_ref_stack.reset() - self.to_ref_stack.reset() - - for i in range(self._param_layout.count()): - item = self._param_layout.itemAt(i) - if item is not None: - widget = item.widget() - if widget is not None: - if hasattr(widget, "reset"): - widget.reset() - - def submit_mapper_deferred( - self, mapper: QtWidgets.QDataWidgetMapper, *args, **kwargs - ): - """ - Call 'submit' on a data widget mapper after a brief delay. This - allows a modified widget to be repainted before updating the - current config, which may have expensive side effects. - """ - QtCore.QTimer.singleShot(10, mapper.submit) - - def update_transform_tab_icons(self) -> None: - """ - Update transform tab icons, indicating which of the tabs - contain transforms. - """ - for tf_edit_stack in (self.from_ref_stack, self.to_ref_stack): - self._on_transform_edited(tf_edit_stack) - - def _on_transform_edited(self, tf_edit_stack: TransformEditStack) -> None: - """ - :param tf_edit_stack: Transform stack containing the - last edited transform edit widget. - """ - if self.__has_transforms__: - if tf_edit_stack.transform_count(): - self.tabs.setTabIcon( - self.tabs.indexOf(tf_edit_stack), - self._from_ref_icon - if tf_edit_stack == self.from_ref_stack - else self._to_ref_icon, - ) - else: - self.tabs.setTabIcon( - self.tabs.indexOf(tf_edit_stack), - self._no_from_ref_icon - if tf_edit_stack == self.from_ref_stack - else self._no_to_ref_icon, - ) - - -class BaseConfigItemEdit(QtWidgets.QWidget): - """ - Widget for editing an item model for the current config. - """ - - # Corresponding BaseConfigItemParamEdit type - __param_edit_type__: type = None - - # Whether there are many config items, which need to be managed through an - # item list. - __has_list__: bool = True - - # Whether the primary widgets of this item edit should be wrapped in a splitter. - # 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: - """ - :return: Item type icon - """ - return cls.__param_edit_type__.__model_type__.item_type_icon() - - @classmethod - 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 - ) - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.param_edit = self.__param_edit_type__() - if self.__has_list__: - self.param_edit.setEnabled(False) - - 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() - ) - self.list.view.installEventFilter(self) - self.list.current_row_changed.connect(self._on_current_row_changed) - - model.item_selection_requested.connect( - lambda index: self.list.set_current_row(index.row()) - ) - 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) - - try: - 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 - - # NOTE: Using the data widget mapper to set/get transforms results in consistent - # crashing, presumably due to transform references being garbage collected - # 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.to_ref_stack.edited.connect(self._on_to_ref_edited) - model.dataChanged.connect(self._on_data_changed) - - # Layout - if self.__has_splitter__ is None: - self.__has_splitter__ = self.__has_list__ - - if self.__has_splitter__: - self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) - self.splitter.setOpaqueResize(False) - if self.__has_list__: - self.splitter.addWidget(self.list) - self.splitter.addWidget(self.param_edit) - - layout = QtWidgets.QVBoxLayout() - if self.__has_splitter__: - layout.addWidget(self.splitter) - else: - layout.addWidget(self.param_edit) - self.setLayout(layout) - - @property - def model(self) -> BaseConfigItemModel: - return self.param_edit.model - - def set_splitter_sizes(self, from_sizes: list[int]) -> None: - """ - Update splitter to match the provided sizes. - - :param from_sizes: Sizes to match, with emphasis on matching - the first index. - """ - if self.__has_splitter__: - self.splitter.setSizes( - adapt_splitter_sizes(from_sizes, self.splitter.sizes()) - ) - - def eventFilter( - self, watched: QtCore.QObject, event: QtCore.QEvent - ) -> bool: - """ - Handle setting subscription for the current item's transform on - number key press. - """ - if ( - self.__has_list__ - and watched == self.list.view - and event.type() == QtCore.QEvent.KeyRelease - and event.key() - in ( - QtCore.Qt.Key_0, - QtCore.Qt.Key_1, - QtCore.Qt.Key_2, - QtCore.Qt.Key_3, - QtCore.Qt.Key_4, - QtCore.Qt.Key_5, - QtCore.Qt.Key_6, - QtCore.Qt.Key_7, - QtCore.Qt.Key_8, - QtCore.Qt.Key_9, - ) - ): - current_index = self.list.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 - ) - return True - - return False - - @QtCore.Slot(int) - def _on_current_row_changed(self, row: int) -> None: - """ - Load the item defined in the model at the specified row. - """ - self.param_edit.setEnabled(row >= 0) - if row < 0: - self.param_edit.reset() - else: - 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.set_transform( - model.data( - model.index( - row, - self.param_edit.__from_ref_column_desc__.column, - ), - QtCore.Qt.EditRole, - ) - ) - self.param_edit.to_ref_stack.set_transform( - model.data( - model.index( - row, - self.param_edit.__to_ref_column_desc__.column, - ), - QtCore.Qt.EditRole, - ) - ) - self.param_edit.update_transform_tab_icons() - - @QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex, list) - def _on_data_changed( - self, - top_left: QtCore.QModelIndex, - bottom_right: QtCore.QModelIndex, - roles=(), - ) -> None: - """ - Manually update transform stacks from model, on model data - change. - """ - if QtCore.Qt.EditRole not in roles: - return - - if self.param_edit.__has_transforms__: - column = top_left.column() - if column == self.param_edit.__from_ref_column_desc__.column: - with SignalsBlocked(self.param_edit.from_ref_stack): - self.param_edit.from_ref_stack.set_transform( - self.model.data(top_left, QtCore.Qt.EditRole) - ) - elif column == self.param_edit.__to_ref_column_desc__.column: - with SignalsBlocked(self.param_edit.to_ref_stack): - self.param_edit.to_ref_stack.set_transform( - self.model.data(top_left, QtCore.Qt.EditRole) - ) - - def _on_from_ref_edited(self) -> None: - """ - Manually update model from transform stack, on transform - change. - """ - if self.param_edit.__has_transforms__ and self.__has_list__: - model = self.model - - current_index = self.list.current_index() - if current_index is not None: - model.setData( - model.index( - current_index.row(), - self.param_edit.__from_ref_column_desc__.column, - ), - self.param_edit.from_ref_stack.transform(), - ) - - def _on_to_ref_edited(self) -> None: - """ - Manually update model from transform stack, on transform - change. - """ - if self.param_edit.__has_transforms__ and self.__has_list__: - model = self.model - - current_index = self.list.current_index() - if current_index is not None: - model.setData( - model.index( - current_index.row(), - self.param_edit.__to_ref_column_desc__.column, - ), - self.param_edit.to_ref_stack.transform(), - ) diff --git a/src/apps/ocioview/ocioview/items/config_item_model.py b/src/apps/ocioview/ocioview/items/config_item_model.py deleted file mode 100644 index 19505c1058..0000000000 --- a/src/apps/ocioview/ocioview/items/config_item_model.py +++ /dev/null @@ -1,830 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import logging -from dataclasses import dataclass -from typing import Any, Optional, Type, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..transform_manager import TransformManager, TransformAgent -from ..undo import ItemModelUndoCommand, ConfigSnapshotUndoCommand -from ..utils import get_glyph_icon, next_name, item_type_label - - -@dataclass -class ColumnDesc: - """ - Dataclass which describes an item model column with its label and - data type. - """ - - column: int - label: str - type: type - - -class BaseConfigItemModel(QtCore.QAbstractTableModel): - """ - Abstract item model class for editing one type of OCIO config - object (ColorSpace, Look, etc.) in the current config. - """ - - item_renamed = QtCore.Signal(str, str) - item_added = QtCore.Signal(str) - item_moved = QtCore.Signal() - item_removed = QtCore.Signal() - item_selection_requested = QtCore.Signal(QtCore.QModelIndex) - warning_raised = QtCore.Signal(str) - - # Agents broadcast transform and name changes to subscribers - _tf_agents = [TransformAgent(i) for i in range(10)] - - # Implementations must include a name column, and define all other - # implemented column description constants here. - NAME = ColumnDesc(0, "Name", str) - - COLUMNS = [NAME] - """Ordered model columns.""" - - NULL_INDEX = QtCore.QModelIndex() - """ - Default constructed (null) model index, indicating an invalid or - unused index. - """ - - # OCIO config object type this model manages. - __item_type__: type = None - - # OCIO config object type label for use in GUI components, generated on first call - # to ``item_type_label()``. - __item_type_label__: str = None - - # QtAwesome glyph name to use for this item type's icon - __icon_glyph__: str = None - - # Item type icon, loaded on first call to ``transform_type_icon()``. - __icon__: QtGui.QIcon = None - - @classmethod - 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 - ) - return cls.__icon__ - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - """ - :param plural: Whether label should be plural - :return: Friendly type name - """ - if cls.__item_type_label__ is None: - cls.__item_type_label__ = item_type_label(cls.__item_type__) - if plural: - return cls.__item_type_label__ + "s" - else: - return cls.__item_type_label__ - - @classmethod - def get_transform_agent(cls, slot: int) -> Optional[TransformAgent]: - """ - Get the transform subscription agent for the specified slot. - - :param slot: Subscription slot number between 1-10 - :return: Transform subscription agent, or None for an invalid - slot number. - """ - if 0 <= slot < 10: - return cls._tf_agents[slot] - else: - return None - - @classmethod - def has_presets(cls) -> bool: - """ - Subclasses must indicate whether the model supports adding - preset items. - - :return: Whether presets are supported - """ - return False - - @classmethod - def requires_presets(cls) -> bool: - """ - Subclasses must indicate whether presets are required (only - presets can be added to model). - - :return: Whether presets are required - """ - return False - - @classmethod - def get_presets(cls) -> Optional[Union[list[str], dict[str, QtGui.QIcon]]]: - """ - Subclasses may return preset items to make available in a view. - - :return: list of preset names or dictionary of preset names and - icons. - """ - return None - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Prefix for new items without provided names - self._item_prefix = f"{self.__item_type__.__name__}_" - - # Temporary item storage for when rebuilding config section - self._items = None - - def reset(self) -> None: - """Reset all model data.""" - self.beginResetModel() - self._reset_cache() - self.endResetModel() - - # Request selection of first item - item_names = self.get_item_names() - if item_names: - first_item_index = self.get_index_from_item_name(item_names[0]) - if first_item_index != self.NULL_INDEX: - self.item_selection_requested.emit(first_item_index) - - def repaint(self) -> None: - """Force all items to be repainted in all views.""" - self.dataChanged.emit( - self.index(0, self.NAME.column), - self.index(self.rowCount() - 1, self.NAME.column), - ) - - def add_preset(self, preset_name: str) -> int: - """ - Subclasses may implement preset item behavior in this method. - By default, it does nothing. - - :param preset_name: Name of preset to add - :return: Added item row - """ - return -1 - - def create_item(self, name: Optional[str] = None) -> int: - """ - Create a new item and add it to the current config, generating - a unique name if none is provided. - - :param name: Optional item name - :return: Item row - """ - row = -1 - - if not name: - item_names = self.get_item_names() + ConfigCache.get_all_names() - name = next_name(self._item_prefix, item_names) - - with ConfigSnapshotUndoCommand( - f"Create {self.item_type_label()}", model=self, item_name=name - ): - self._new_item(name) - index = self.get_index_from_item_name(name) - - # Was an item created? - if index is not None: - row = index.row() - - self.beginInsertRows(self.NULL_INDEX, row, row) - self.endInsertRows() - self.item_added.emit(name) - - return row - - def move_item_up(self, item_name: str) -> bool: - """ - Move the named item up one row, if possible. - - :param item_name: Name of item to move - :return: Whether the item was moved - """ - item_names = self.get_item_names() - if item_name not in item_names: - return False - - src_row = item_names.index(item_name) - dst_row = max(0, src_row - 1) - - if dst_row == src_row: - return False - - return self.moveRows( - self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row - ) - - def move_item_down(self, item_name: str) -> bool: - """ - Move the named item down one row, if possible. - - :param item_name: Name of item to move - :return: Whether the item was moved - """ - item_names = self.get_item_names() - if item_name not in item_names: - return False - - src_row = item_names.index(item_name) - dst_row = min(len(item_names) - 1, src_row + 1) - - if dst_row == src_row: - return False - - 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 - - def rowCount(self, parent: QtCore.QModelIndex = NULL_INDEX) -> int: - """ - :return: Number of items defined in the current config - """ - return len(self._get_items()) - - def columnCount(self, parent: QtCore.QModelIndex = NULL_INDEX) -> int: - return len(self.COLUMNS) - - def headerData( - self, - column: int, - orientation: QtCore.Qt.Orientation, - role: int = QtCore.Qt.DisplayRole, - ) -> Any: - """ - :return: Column labels - """ - 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: - """ - :return: Item data pulled from the current config for the - index-referenced column. - """ - item, column_desc = self._get_item_and_column(index) - if item is None: - return None - - if role == QtCore.Qt.DisplayRole: - return self._get_display(item, column_desc) - elif role == QtCore.Qt.EditRole: - return self._get_value(item, column_desc) - elif role == QtCore.Qt.DecorationRole: - return self._get_icon(item, column_desc) - elif role == QtCore.Qt.BackgroundRole: - return self._get_bg_color(item, column_desc) - elif role == QtCore.Qt.CheckStateRole: - # Get check state from configured column - checked_column_desc = self._get_checked_column() - if checked_column_desc is not None: - checked = self._get_value(item, checked_column_desc) - return QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked - - return None - - def setData( - self, - index: QtCore.QModelIndex, - value: Any, - role: int = QtCore.Qt.EditRole, - ) -> bool: - """ - Push modified item data to the current config for the - index-referenced column. - """ - if role not in (QtCore.Qt.EditRole, QtCore.Qt.CheckStateRole): - return False - - item, column_desc = self._get_item_and_column(index) - if item is None: - return False - - if role == QtCore.Qt.CheckStateRole: - # Route check state to configured column - checked_column_desc = self._get_checked_column() - 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 - role = QtCore.Qt.EditRole - - undo_cmd_type = self._get_undo_command_type(column_desc) - - current_value = self._get_value(item, column_desc) - data_changed = current_value != value - - if data_changed: - if not isinstance(index, QtCore.QPersistentModelIndex): - if undo_cmd_type == ItemModelUndoCommand: - # Add undo command to undo stack on initial invocation, which will - # cycle back through this method by calling 'redo' with the new - # persistent index. - ItemModelUndoCommand( - self._get_undo_command_text(index, column_desc), - QtCore.QPersistentModelIndex(index), - value, - current_value, - ) - return False - - if undo_cmd_type == ConfigSnapshotUndoCommand: - # Capture immediate change and side effects from the dataChanged - # signal with undo command. - with ConfigSnapshotUndoCommand( - self._get_undo_command_text(index, column_desc), - model=self, - item_name=self.get_item_name(index), - ): - self._set_value(item, column_desc, value, index) - self.dataChanged.emit(index, index, [role]) - else: - self._set_value(item, column_desc, value, index) - self.dataChanged.emit(index, index, [role]) - - return data_changed - - def insertRows( - self, row: int, count: int, parent: QtCore.QModelIndex = NULL_INDEX - ) -> bool: - """ - Create ``count`` new items and add them to the current - config at ``row`` index. - - Due to new config items generally being appended to the - relevant config section, all similar items are first removed - from the config (after being preserved in memory), and then - re-added with the new items inserted at the requested index. - """ - self.beginInsertRows(parent, row, row + count - 1) - - preserved_items = self._get_items(preserve=True) - item_names = self.get_item_names() + ConfigCache.get_all_names() - - new_names = [] - for _ in range(count): - name = next_name(self._item_prefix, item_names + new_names) - new_names.append(name) - - with ConfigSnapshotUndoCommand( - f"Add {self.item_type_label()}", model=self, item_name=new_names[0] - ): - self._clear_items() - - if preserved_items: - for other_row, item in enumerate(preserved_items): - if other_row == row: - for name in new_names: - self._new_item(name) - self._add_item(item) - else: - for name in new_names: - self._new_item(name) - - self.endInsertRows() - - for name in new_names: - self.item_added.emit(name) - - return True - - def moveRows( - self, - src_parent: QtCore.QModelIndex, - src_row: int, - count: int, - dst_parent: QtCore.QModelIndex, - dst_row: int, - ) -> bool: - """ - Move ``count`` items to the specified destination index in the - current config. - - Due to new config items generally being appended to the - relevant config section, all items are first removed from the - config (after being preserved in memory), and then re-added - in new order. - """ - dst_row += 1 if dst_row > src_row else 0 - - if self.beginMoveRows( - src_parent, src_row, src_row + count - 1, dst_parent, dst_row - ): - all_names = self.get_item_names() - all_items = { - name: item - for name, item in zip( - all_names, self._get_items(preserve=True) - ) - } - - insert_before = None - if dst_row < len(all_items): - insert_before = all_names[dst_row] - - move_names = [ - all_names.pop(i) - for i in reversed(range(src_row, src_row + count)) - ] - - if insert_before is not None: - new_dst_row = all_names.index(insert_before) - else: - new_dst_row = len(all_names) - - with ConfigSnapshotUndoCommand( - 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]) - - self._clear_items() - for name in all_names: - self._add_item(all_items[name]) - - self.endMoveRows() - self.item_moved.emit() - - return True - - return False - - def removeRows( - self, row: int, count: int, parent: QtCore.QModelIndex = NULL_INDEX - ) -> bool: - """ - Remove ``count`` items from the current config, starting at - ``row`` index. - """ - items = self._get_items() - item_names = self.get_item_names() - num_items = len(items) - 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) - - for i in reversed(remove_rows): - self._remove_item(items[i]) - - self.item_removed.emit() - self.endRemoveRows() - - # Warn user about refused item removals - if could_not_remove: - item_warning_lines = [] - 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(could_not_remove)} " - f"{self.item_type_label(plural=len(could_not_remove) != 1).lower()} could " - f"not be removed:

{item_warnings}" - ) - - return True - - def get_item_names(self) -> list[str]: - """ - :return: All item names - """ - raise NotImplementedError - - def get_item_name(self, index: QtCore.QModelIndex) -> Optional[str]: - """ - :param index: Model index for item - :return: Item name, if available - """ - return self.data(self.index(index.row(), self.NAME.column)) - - def format_subscription_item_label( - self, item_name_or_index: Union[str, QtCore.QModelIndex], **kwargs - ) -> Optional[str]: - """ - Format item name into a per-model unique label for tracking - transform subscriptions. - - :param item_name_or_index: Item name or model index for item - :return: Subscription unique item label - """ - if isinstance(item_name_or_index, QtCore.QModelIndex): - item_label = self.get_item_name(item_name_or_index) - else: - item_label = item_name_or_index - return f"{item_label} [{self.item_type_label().lower()}]" - - def extract_subscription_item_name(self, item_label: str) -> str: - """ - Unformat item name from its per-model unique name for tracking - transform subscriptions. - - :param item_label: Subscription unique item label - :return: Extracted item name - """ - suffix = f" [{self.item_type_label().lower()}]" - if item_label.endswith(suffix): - return item_label[: -len(suffix)] - else: - return item_label - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - """ - :param item_label: Subscription unique label of item to get - transform for. - :return: Forward and inverse item transforms, or None if either - is not defined. - """ - return None, None - - def get_index_from_item_name( - self, item_name: str - ) -> Optional[QtCore.QModelIndex]: - """ - Lookup the model index for the named item. - - :param item_name: Item name to lookup - :return: Item model index, if found - """ - indexes = self.match( - self.index(0, self.NAME.column), - QtCore.Qt.DisplayRole, - item_name, - 1, - QtCore.Qt.MatchExactly | QtCore.Qt.MatchWrap, - ) - if indexes: - return indexes[0] - else: - return None - - def _reset_cache(self) -> None: - """Reset internal config cache.""" - self._items = None - - def _get_item_and_column( - self, index: QtCore.QModelIndex - ) -> tuple[Optional[__item_type__], Optional[ColumnDesc]]: - items = self._get_items() - if items: - try: - return items[index.row()], self.COLUMNS[index.column()] - except IndexError: - # Item may have been removed - logging.warning(f"{self} index {index} is invalid") - return None, None - - def _get_undo_command_type( - self, column_desc: ColumnDesc - ) -> Type[QtGui.QUndoCommand]: - """ - Support overriding the undo command type used to - track data changes, per column. - - :param column_desc: Description of column being modified - :return: Undo command type to track change - """ - return ItemModelUndoCommand - - def _get_undo_command_text( - self, index: QtCore.QModelIndex, column_desc: ColumnDesc - ) -> str: - """ - Format undo/redo action text from the item associated with the - provided index and column. - - :param index: Model index being modified - :param column_desc: Description of column being modified - :return: Undo command text - """ - # Get item name for undo/redo action text - item_name = self.get_item_name(index) - if item_name: - item_desc = f" ({item_name})" - else: - item_desc = "" - - return f"Edit {self.item_type_label()} {column_desc.label}{item_desc}" - - def _can_item_be_removed(self, item: __item_type__) -> tuple[bool, str]: - """ - Subclasses can override this method to prevent an item from - being deleted by a user. Items should not be deleted when they - are referenced elsewhere in the config. - - :param item: Item to check - :return: Whether the item can safely be deleted, and a - descriptive message with a reason if it can't. - """ - return True, "" - - def _get_display( - self, item: __item_type__, column_desc: ColumnDesc - ) -> str: - """ - :return: Display role value for a given model column - """ - value = self._get_value(item, column_desc) - - if column_desc.type == ocio.Transform: - return value.__class__.__name__ - elif column_desc.type == list: - return ", ".join(map(str, value)) - elif hasattr(column_desc.type, "__members__"): - return value.name - else: - return str(value) - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - """ - :return: Background role value for a given model column - """ - return None - - def _get_subscription_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - """ - If the item is set as a transform subscription, return a - color for its subscription slot. - """ - slot = TransformManager.get_subscription_slot( - self, - self.format_subscription_item_label( - self._get_value(item, column_desc) - ), - ) - return TransformManager.get_subscription_slot_color( - slot, saturation=0.25, value=0.25 - ) - - def _get_subscription_icon( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - """ - If the item is set as a transform subscription, return a number - icon for its subscription slot. - """ - slot = TransformManager.get_subscription_slot( - self, - self.format_subscription_item_label( - self._get_value(item, column_desc) - ), - ) - return TransformManager.get_subscription_slot_icon(slot) - - def _get_icon( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - """ - :return: Icon for a given model column - """ - if column_desc == self.NAME: - return self.item_type_icon() - else: - return None - - def _get_items(self, preserve: bool = False) -> list[__item_type__]: - """ - :param preserve: Whether to preserve the config items in - local memory prior to returning them. All items should - be able to be removed from the current config without - incurring data loss when re-adding them later. - :return: All items of the configured type from the current - config. - """ - raise NotImplementedError - - def _clear_items(self) -> None: - """ - Remove all items of the configured type from the current - config. - """ - raise NotImplementedError - - def _add_item(self, item: __item_type__) -> None: - """ - Add the provided item to the current config. - """ - raise NotImplementedError - - def _remove_item(self, item: __item_type__) -> None: - """ - Remove the provided item from the current config. - """ - raise NotImplementedError - - def _new_item(self, name: str) -> None: - """ - Create a new item with the specified name and add it to the - current config. - """ - raise NotImplementedError - - def _get_checked_column(self) -> Optional[ColumnDesc]: - """ - :return: Column description for the column that holds whether - an item is checked (which can differ from the column that - displays a checkbox). Return `None` if items have no check - state (the default). - """ - return None - - def _update_tf_subscribers( - self, item_name: str, prev_item_name: Optional[str] = None - ) -> None: - """ - Broadcast transform and/or name changes to item transform - subscribers. - - :param item_name: Name of changed item - :param prev_item_name: Optional previous item name, if the - name changed. - """ - # Name adjustment may be needed for unique item/transform identifiers within - # 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 - ) - else: - prev_item_label = None - - # Is item set as a subscription? - slot = TransformManager.get_subscription_slot( - self, prev_item_label or item_label - ) - if slot != -1: - # Broadcast name change - if prev_item_label and prev_item_label != item_label: - self._tf_agents[slot].item_name_changed.emit(item_label) - - # Broadcast transform change - self._tf_agents[slot].item_tf_changed.emit( - *self.get_item_transforms(item_label) - ) - - def _get_value(self, item: __item_type__, column_desc: ColumnDesc) -> Any: - """ - :return: Item parameter value referred to by the requested - model column. This pulls data from the current config. - """ - raise NotImplementedError - - def _set_value( - self, - item: __item_type__, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - """ - Set the item parameter value referred to by the specified model - column. This pushes data to the current config. - - :return: A set of roles that should be updated for this model - item in all views. - """ - raise NotImplementedError diff --git a/src/apps/ocioview/ocioview/items/config_properties_edit.py b/src/apps/ocioview/ocioview/items/config_properties_edit.py deleted file mode 100644 index 9eb42b54ea..0000000000 --- a/src/apps/ocioview/ocioview/items/config_properties_edit.py +++ /dev/null @@ -1,151 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from pathlib import Path -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtWidgets - -from ..constants import RGB, ICON_SIZE_ITEM -from ..widgets import ( - ComboBox, - LineEdit, - PathEdit, - FloatEditArray, - TextEdit, - StringListWidget, - StringMapTableWidget, -) -from ..utils import get_glyph_icon -from .config_properties_model import ConfigPropertiesModel -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit - - -class ConfigPropertiesParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing root properties for the current config. - """ - - __model_type__ = ConfigPropertiesModel - __has_transforms__ = False - __has_tabs__ = True - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.version_edit = ComboBox() - self.version_edit.addItems(self.model.supported_versions()) - self.description_edit = TextEdit() - self.env_vars_table = StringMapTableWidget( - ("Name", "Default Value"), - item_icon=get_glyph_icon("mdi6.variable", size=ICON_SIZE_ITEM), - default_key_prefix="ENV_VAR_", - default_value="value", - ) - self.search_path_list = StringListWidget( - item_icon=get_glyph_icon("ph.file-search", size=ICON_SIZE_ITEM), - get_item=self._get_search_path, - ) - self.working_dir_edit = PathEdit(QtWidgets.QFileDialog.Directory) - self.family_separator_edit = LineEdit() - self.default_luma_coefs_edit = FloatEditArray(RGB) - - # 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.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.FAMILY_SEPARATOR.label, self.family_separator_edit - ) - self._param_layout.addRow( - self.model.DEFAULT_LUMA_COEFS.label, self.default_luma_coefs_edit - ) - - def _get_search_path(self) -> Optional[str]: - """ - Browse filesystem for new search path, making the returned - directory path relative to the working directory if it is a - child directory. - - :return: Search path string to add, or None to bail - """ - config = ocio.GetCurrentConfig() - working_dir_str = config.getWorkingDir() - - search_path_str = QtWidgets.QFileDialog.getExistingDirectory( - self, "Choose Search Path", dir=working_dir_str - ) - if search_path_str: - working_dir = Path(working_dir_str) - search_path = Path(search_path_str) - if search_path.is_relative_to(working_dir): - return search_path.relative_to(working_dir).as_posix() - else: - return search_path.as_posix() - - return None - - -class ConfigPropertiesEdit(BaseConfigItemEdit): - """ - Widget for editing root properties in the current config. - """ - - __param_edit_type__ = ConfigPropertiesParamEdit - __has_list__ = False - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Map widgets to model columns - 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.param_edit.env_vars_table, model.ENVIRONMENT_VARS.column - ) - self.mapper.addMapping( - self.param_edit.search_path_list, model.SEARCH_PATH.column - ) - 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.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 - ) - - # Reload sole item on reset - model.modelReset.connect(lambda: self.mapper.setCurrentIndex(0)) - - # Initialize - self.mapper.setCurrentIndex(0) diff --git a/src/apps/ocioview/ocioview/items/config_properties_model.py b/src/apps/ocioview/ocioview/items/config_properties_model.py deleted file mode 100644 index 247a7fb428..0000000000 --- a/src/apps/ocioview/ocioview/items/config_properties_model.py +++ /dev/null @@ -1,166 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import logging -from pathlib import Path -from typing import Any, Optional - -from PySide6 import QtCore -import PyOpenColorIO as ocio - -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -logger = logging.getLogger(__name__) - - -class ConfigPropertiesModel(BaseConfigItemModel): - """ - Item model for editing root properties in the current config. - """ - - NAME = ColumnDesc(0, "Name", str) - VERSION = ColumnDesc(1, "Version", str) - DESCRIPTION = ColumnDesc(2, "Description", str) - ENVIRONMENT_VARS = ColumnDesc(3, "Environment Vars", list) - SEARCH_PATH = ColumnDesc(4, "Search Path", list) - WORKING_DIR = ColumnDesc(5, "Working Dir", Path) - FAMILY_SEPARATOR = ColumnDesc(6, "Family Separator", str) - DEFAULT_LUMA_COEFS = ColumnDesc(7, "Default Luma Coefs", list) - - # fmt: off - COLUMNS = sorted([ - NAME, VERSION, DESCRIPTION, ENVIRONMENT_VARS, SEARCH_PATH, WORKING_DIR, - FAMILY_SEPARATOR, DEFAULT_LUMA_COEFS, - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = ocio.Config - __icon_glyph__ = "ph.sliders" - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - return "Properties" - - def supported_versions(self) -> list[str]: - """ - Infer all supported config versions from a test config. - """ - versions = [] - test_config = ocio.Config() - - major_version = 1 - while True: - try: - # Setting the major version will set the minor version to the most - # recent value. - test_config.setMajorVersion(major_version) - max_minor_version = test_config.getMinorVersion() - - # Iterate minor versions at and below the inferred maximum to test - # which are valid. - for minor_version in range(max_minor_version + 1): - try: - # Test the validity of this major/minor pair, adding it to the - # output if no exception is raised. - test_config.setVersion(major_version, minor_version) - versions.append(f"{major_version}.{minor_version}") - except ocio.Exception: - continue - - except ocio.Exception: - break - major_version += 1 - - return versions - - def get_item_names(self) -> list[str]: - return [] - - def _get_item_and_column( - self, index: QtCore.QModelIndex - ) -> tuple[Optional[__item_type__], Optional[ColumnDesc]]: - return ocio.GetCurrentConfig(), self.COLUMNS[index.column()] - - def _get_value(self, item: ocio.Config, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.getName() - elif column_desc == self.VERSION: - return f"{item.getMajorVersion()}.{item.getMinorVersion()}" - elif column_desc == self.DESCRIPTION: - return item.getDescription() - elif column_desc == self.ENVIRONMENT_VARS: - return [ - (name, item.getEnvironmentVarDefault(name)) - for name in item.getEnvironmentVarNames() - ] - elif column_desc == self.SEARCH_PATH: - return item.getSearchPaths() - elif column_desc == self.WORKING_DIR: - return Path(item.getWorkingDir()) - elif column_desc == self.FAMILY_SEPARATOR: - return item.getFamilySeparator() - elif column_desc == self.DEFAULT_LUMA_COEFS: - return item.getDefaultLumaCoefs() - - # Invalid column - return None - - def _set_value( - self, - item: ocio.Config, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - # Update parameters - if column_desc == self.NAME: - item.setName(value) - elif column_desc == self.VERSION: - major, minor = tuple(map(int, value.split("."))) - try: - item.setVersion(major, minor) - except ocio.Exception as e: - logger.warning(str(e)) - elif column_desc == self.DESCRIPTION: - item.setDescription(value) - elif column_desc == self.ENVIRONMENT_VARS: - item.clearEnvironmentVars() - for name, default in value: - item.addEnvironmentVar(name, default) - elif column_desc == self.SEARCH_PATH: - item.clearSearchPaths() - for path in value: - item.addSearchPath(Path(path).as_posix()) - elif column_desc == self.WORKING_DIR: - item.setWorkingDir(value.as_posix()) - elif column_desc == self.FAMILY_SEPARATOR: - item.setFamilySeparator(value) - elif column_desc == self.DEFAULT_LUMA_COEFS: - item.setDefaultLumaCoefs(value) - - # There's one singleton config, so short circuit methods that aren't needed - def rowCount(self, *args, **kwargs) -> int: - return 1 - - def insertRows(self, *args, **kwargs) -> bool: - return False - - def removeRows(self, *args, **kwargs) -> bool: - return False - - def _get_items(self, *args, **kwargs) -> list: - return [] - - def _clear_items(self, *args, **kwargs) -> None: - pass - - def _add_item(self, *args, **kwargs) -> None: - pass - - def _remove_item(self, *args, **kwargs) -> None: - pass - - def _new_item(self, *args, **kwargs) -> None: - pass diff --git a/src/apps/ocioview/ocioview/items/delegates.py b/src/apps/ocioview/ocioview/items/delegates.py deleted file mode 100644 index 75499e651b..0000000000 --- a/src/apps/ocioview/ocioview/items/delegates.py +++ /dev/null @@ -1,192 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..config_cache import ConfigCache -from ..widgets import CallbackComboBox -from .role_model import RoleModel - - -class ColorSpaceDelegate(QtWidgets.QStyledItemDelegate): - """ - Delegate for choosing a color space directly within a list view. - """ - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Keep track of previous value to detect duplicate entries - self._prev_value = None - - def createEditor( - self, - parent: QtWidgets.QWidget, - option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex, - ) -> QtWidgets.QWidget: - """ - Create searchable combo box with available options/presets. - """ - editor = CallbackComboBox( - get_items=ConfigCache.get_color_space_names, editable=True, parent=parent - ) - editor.completer().setCompletionMode(QtWidgets.QCompleter.PopupCompletion) - return editor - - def setEditorData( - self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex - ) -> None: - """Pull data from model.""" - self._prev_value = index.data(QtCore.Qt.EditRole) - editor.setCurrentText(self._prev_value) - editor.lineEdit().selectAll() - - def updateEditorGeometry( - self, - editor: QtWidgets.QWidget, - option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex, - ) -> None: - """Position delegate widget directly over table cell.""" - editor.setGeometry(option.rect) - - def setModelData( - self, - editor: QtWidgets.QWidget, - model: QtCore.QAbstractItemModel, - index: QtCore.QModelIndex, - ) -> None: - """Validate and push data back to model.""" - value = editor.currentText() - model = index.model() - - # Verify that this color space is not already present in the model - if value != self._prev_value: - other_indices = model.match( - model.index(0, 0), - QtCore.Qt.DisplayRole, - value, - flags=QtCore.Qt.MatchExactly | QtCore.Qt.MatchWrap, - ) - if len(other_indices) > 1: - return - - # Verify manually entered value is a color space - config = ocio.GetCurrentConfig() - color_space = config.getColorSpace(value) - if color_space is None: - return - - model.setData(index, value, QtCore.Qt.EditRole) - - -class RoleDelegate(QtWidgets.QStyledItemDelegate): - """ - Delegate for editing role names and color spaces directly within - a role table view. - """ - - def __init__(self, model: RoleModel, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Keep model reference to check existing roles in use - self._model = model - - # Keep track of previous value to detect duplicate roles - self._prev_value = None - - def createEditor( - self, - parent: QtWidgets.QWidget, - option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex, - ) -> QtWidgets.QWidget: - """ - Create searchable combo box with available options/presets. - """ - column = index.column() - - if column == RoleModel.NAME.column: - get_items = self._get_available_roles - elif column == RoleModel.COLOR_SPACE.column: - get_items = ConfigCache.get_color_space_names - else: - raise NotImplementedError - - widget = CallbackComboBox(get_items=get_items, editable=True, parent=parent) - widget.completer().setCompletionMode(QtWidgets.QCompleter.PopupCompletion) - return widget - - def setEditorData( - self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex - ) -> None: - """Pull data from model.""" - self._prev_value = index.data(QtCore.Qt.EditRole) - editor.setCurrentText(self._prev_value) - editor.lineEdit().selectAll() - - def updateEditorGeometry( - self, - editor: QtWidgets.QWidget, - option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex, - ) -> None: - """Position delegate widget directly over table cell.""" - editor.setGeometry(option.rect) - - def setModelData( - self, - editor: QtWidgets.QWidget, - model: QtCore.QAbstractItemModel, - index: QtCore.QModelIndex, - ) -> None: - """Validate and push data back to model.""" - value = editor.currentText() - model = index.model() - column = index.column() - - if column == RoleModel.NAME.column: - # Verify that this role is not already set by a different item - if value != self._prev_value: - other_indices = model.match( - model.index(0, 0), - QtCore.Qt.DisplayRole, - value, - flags=QtCore.Qt.MatchExactly | QtCore.Qt.MatchWrap, - ) - if len(other_indices) > 1: - return - - elif column == RoleModel.COLOR_SPACE.column: - # Verify manually entered value is a color space - config = ocio.GetCurrentConfig() - color_space = config.getColorSpace(value) - if color_space is None: - return - - else: - raise NotImplementedError - - model.setData(index, value, QtCore.Qt.EditRole) - - def _get_available_roles(self) -> list[str]: - """ - :return: All builtin color space roles that aren't already - defined by the model, for use in the preset menu. - """ - defined_roles = [] - for i in range(self._model.rowCount()): - defined_roles.append( - self._model.data(self._model.index(i, 0), QtCore.Qt.EditRole) - ) - - # Include current value to allow easy bailing from preset menu - return [self._prev_value] + [ - role - for role in ConfigCache.get_builtin_color_space_roles() - if role not in defined_roles - ] diff --git a/src/apps/ocioview/ocioview/items/display_model.py b/src/apps/ocioview/ocioview/items/display_model.py deleted file mode 100644 index 78d80a095f..0000000000 --- a/src/apps/ocioview/ocioview/items/display_model.py +++ /dev/null @@ -1,255 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from dataclasses import dataclass, field -from typing import Any, Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..config_cache import ConfigCache -from ..utils import next_name -from .config_item_model import ColumnDesc, BaseConfigItemModel -from .utils import ViewType, get_view_type - - -@dataclass -class View: - """Individual view storage.""" - - type: ViewType - name: str - color_space: str - view_transform: str = "" - looks: str = "" - rule: str = "" - description: str = "" - - -@dataclass -class Display: - """Individual display storage.""" - - name: str - views: list[View] = field(default_factory=list) - - -class DisplayModel(BaseConfigItemModel): - """ - Item model for editing displays in the current config. This model - also tracks associated views, but is not responsible for editing - views. - """ - - NAME = ColumnDesc(0, "Display", str) - - COLUMNS = [NAME] - - __item_type__ = Display - __icon_glyph__ = "mdi6.monitor" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - ConfigCache.register_reset_callback(self._reset_cache) - - def get_item_names(self) -> list[str]: - return [v.name for v in self._get_items()] - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[Display]: - if ConfigCache.validate() and self._items: - return self._items - - config = ocio.GetCurrentConfig() - - self._items = [] - - # Get views to preserve through display name changes - for name in config.getDisplays(): - display = Display(name) - - # Display-defined views - 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 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) - - return self._items - - def _clear_items(self) -> None: - # Remove all display and views. Shared views are preserved. - config = ocio.GetCurrentConfig() - config.clearDisplays() - - def _add_item(self, item: Display) -> None: - config = ocio.GetCurrentConfig() - for view in item.views: - if view.type == ViewType.VIEW_SHARED: - config.addDisplaySharedView(item.name, view.name) - elif view.type == ViewType.VIEW_DISPLAY: - config.addDisplayView( - item.name, - view.name, - view.view_transform, - view.color_space, - view.looks, - view.rule, - view.description, - ) - else: # VIEW_SCENE - config.addDisplayView( - item.name, view.name, view.color_space, view.looks - ) - - def _remove_item(self, item: Display) -> None: - # Remove all views from display. The display will be removed once it has no - # associated views. Shared views will be disassociated, but preserved. Views - # must be removed in reverse to preserve internal indices. - config = ocio.GetCurrentConfig() - for view in reversed(item.views): - config.removeDisplayView(item.name, view.name) - - def _new_item(self, name: str) -> None: - config = ocio.GetCurrentConfig() - - view_transform = ConfigCache.get_default_view_transform_name() - if not view_transform: - view_transforms = ConfigCache.get_view_transforms() - if view_transforms: - view_transform = view_transforms[0] - - color_space = None - - # A display can't exist without a view, so we'll add an initial view with it. - # Prefer display-referred view if a view transform exists - if view_transform: - color_spaces = ConfigCache.get_color_space_names( - ocio.SEARCH_REFERENCE_SPACE_DISPLAY - ) - if color_spaces: - color_space = color_spaces[0] - - if not color_space: - color_space = ConfigCache.get_default_color_space_name() - - if color_space: - # Generate unique view name - views = ConfigCache.get_views() - new_view = next_name("View_", views) - - # Add new display and view - if view_transform: - config.addDisplayView( - name, - new_view, - viewTransform=view_transform, - displayColorSpaceName=color_space, - ) - else: - config.addDisplayView( - name, new_view, colorSpaceName=color_space - ) - - def _get_value(self, item: Display, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.name - - # Invalid column - return None - - def _set_value( - self, - item: Display, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - item_names = self.get_item_names() - if item.name not in item_names: - return - - prev_item_name = item.name - items = self._get_items() - item_index = item_names.index(item.name) - - # Update parameters - if column_desc == self.NAME: - is_valid = True - config = ocio.GetCurrentConfig() - for view in item.views: - if ( - view.type == ViewType.VIEW_SHARED - and view.color_space == ocio.OCIO_VIEW_USE_DISPLAY_NAME - ): - new_display_color_space = config.getColorSpace(value) - if ( - new_display_color_space is None - or new_display_color_space.getReferenceSpaceType() - != ocio.REFERENCE_SPACE_DISPLAY - ): - self.warning_raised.emit( - f"Display '{item.name}' has one or more shared views which " - f"derive their color space from the display name. No " - f"display color spaces named '{value}' exist. Please " - f"either choose a display name that matches a display " - f"color space name, or remove shared views from this " - f"display that utilize '{ocio.OCIO_VIEW_USE_DISPLAY_NAME}' " - f"for their color space." - ) - is_valid = False - break - - if is_valid: - items[item_index].name = value - - # Make sure local item instance matches item in items list - item = items[item_index] - - if item.name != prev_item_name: - # Rebuild display views with new name - self._clear_items() - for other_item in items: - self._add_item(other_item) - - # Tell views to follow selection to new item - self.item_added.emit(value) - - self.item_renamed.emit(item.name, prev_item_name) diff --git a/src/apps/ocioview/ocioview/items/display_view_edit.py b/src/apps/ocioview/ocioview/items/display_view_edit.py deleted file mode 100644 index 40be4a0c01..0000000000 --- a/src/apps/ocioview/ocioview/items/display_view_edit.py +++ /dev/null @@ -1,124 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from .active_display_view_edit import ActiveDisplayViewEdit -from .shared_view_edit import SharedViewEdit -from .utils import adapt_splitter_sizes -from .view_edit import ViewEdit - - -class DisplayViewEdit(QtWidgets.QWidget): - """ - Widget for editing all displays and views in the current config. - """ - - @classmethod - def item_type_icon(cls) -> QtGui.QIcon: - """ - :return: Item type icon - """ - return get_glyph_icon("mdi6.monitor-eye", size=ICON_SIZE_ITEM) - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - """ - :param plural: Whether label should be plural - :return: Friendly type name - """ - return ViewEdit.item_type_label(plural=plural) - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - self._prev_index = 0 - - # Widgets - self.view_edit = ViewEdit() - self.shared_view_edit = SharedViewEdit() - self.active_display_view_edit = ActiveDisplayViewEdit() - - # Layout - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab( - self.view_edit, - self.view_edit.item_type_icon(), - self.view_edit.item_type_label(plural=True), - ) - self.tabs.addTab( - self.shared_view_edit, - self.shared_view_edit.item_type_icon(), - self.shared_view_edit.item_type_label(plural=True), - ) - self.tabs.addTab( - self.active_display_view_edit, - self.active_display_view_edit.item_type_icon(), - self.active_display_view_edit.item_type_label(plural=True), - ) - self.tabs.currentChanged.connect(self._on_current_changed) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.tabs) - self.setLayout(layout) - - # Connections - self.view_edit.shared_view_selection_requested.connect( - self._on_shared_view_selection_requested - ) - - # Update active display and view lists when display or view lists change - for signal_name in ("item_added", "item_removed", "item_renamed"): - # fmt: off - getattr(self.view_edit.display_model, signal_name).connect( - lambda *args, **kwargs: - self.active_display_view_edit.active_display_edit.reset() - ) - getattr(self.view_edit.model, signal_name).connect( - lambda *args, **kwargs: - self.active_display_view_edit.active_view_edit.reset() - ) - # fmt: on - - @property - def splitter(self) -> QtWidgets.QSplitter: - return self.tabs.currentWidget().splitter - - def set_splitter_sizes(self, from_sizes: list[int]) -> None: - """ - Update splitter to match the provided sizes. - - :param from_sizes: Sizes to match, with emphasis on matching - the first index. - """ - to_widget = self.tabs.currentWidget() - to_widget.splitter.setSizes( - adapt_splitter_sizes(from_sizes, to_widget.splitter.sizes()) - ) - - @QtCore.Slot(int) - def _on_current_changed(self, index: int) -> None: - """Match tab splitter sizes on tab change.""" - from_widget = self.tabs.widget(self._prev_index) - to_widget = self.tabs.widget(index) - - to_widget.splitter.setSizes( - adapt_splitter_sizes( - from_widget.splitter.sizes(), to_widget.splitter.sizes() - ) - ) - - self._prev_index = index - - @QtCore.Slot(str) - def _on_shared_view_selection_requested(self, view: str) -> None: - """ - Switch to the shared view tab and try to make the named view - current. - """ - self.tabs.setCurrentWidget(self.shared_view_edit) - self.shared_view_edit.set_current_view(view) diff --git a/src/apps/ocioview/ocioview/items/file_rule_edit.py b/src/apps/ocioview/ocioview/items/file_rule_edit.py deleted file mode 100644 index d3602a6b81..0000000000 --- a/src/apps/ocioview/ocioview/items/file_rule_edit.py +++ /dev/null @@ -1,203 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from ..widgets import ( - ColorSpaceComboBox, - ExpandingStackedWidget, - FormLayout, - LineEdit, - StringMapTableWidget, -) -from .file_rule_model import FileRuleType, FileRuleModel -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit - - -class FileRuleParamEdit(BaseConfigItemParamEdit): - """Widget for editing the parameters for one file rule.""" - - __model_type__ = FileRuleModel - __has_transforms__ = False - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - self._current_file_rule_type = FileRuleType.RULE_DEFAULT - - # Build stack widget layers - self._param_stack = ExpandingStackedWidget() - - self.name_edits = {} - self.color_space_combos = {} - self.pattern_edits = {} - self.regex_edits = {} - self.extension_edits = {} - self.custom_keys_tables = {} - - for file_rule_type in FileRuleType.__members__.values(): - params_layout = FormLayout() - params_layout.setContentsMargins(0, 0, 0, 0) - - name_edit = LineEdit() - self.name_edits[file_rule_type] = name_edit - params_layout.addRow(self.model.NAME.label, name_edit) - - if file_rule_type != FileRuleType.RULE_OCIO_V1: - 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 - ) - - if file_rule_type == FileRuleType.RULE_BASIC: - pattern_edit = LineEdit() - self.pattern_edits[file_rule_type] = pattern_edit - params_layout.addRow(self.model.PATTERN.label, pattern_edit) - - extension_edit = LineEdit() - self.extension_edits[file_rule_type] = 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, - ): - custom_keys_table = StringMapTableWidget( - ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), - default_key_prefix="key_", - 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 = QtWidgets.QFrame() - params.setLayout(params_layout) - self._param_stack.addWidget(params) - - self._param_layout.removeRow(0) - self._param_layout.addRow(self._param_stack) - - def update_available_params( - self, mapper: QtWidgets.QDataWidgetMapper, file_rule_type: FileRuleType - ) -> None: - """ - Enable the interface needed to edit this rule's type. - """ - self._current_file_rule_type = file_rule_type - - self._param_stack.setCurrentIndex( - [ - FileRuleType.RULE_BASIC, - FileRuleType.RULE_REGEX, - FileRuleType.RULE_OCIO_V1, - FileRuleType.RULE_DEFAULT, - ].index(file_rule_type) - ) - - if file_rule_type in self.name_edits: - 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) - ) - - 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].setEnabled( - file_rule_type != FileRuleType.RULE_OCIO_V1 - ) - - if file_rule_type in self.pattern_edits: - mapper.addMapping( - self.pattern_edits[file_rule_type], self.model.PATTERN.column - ) - self.pattern_edits[file_rule_type].setEnabled( - file_rule_type == FileRuleType.RULE_BASIC - ) - - if file_rule_type in self.regex_edits: - 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].setEnabled( - file_rule_type == FileRuleType.RULE_BASIC - ) - - 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].setEnabled( - file_rule_type - in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX) - ) - - -class FileRuleEdit(BaseConfigItemEdit): - """ - Widget for editing all file rules in the current config. - """ - - __param_edit_type__ = FileRuleParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Clear default mapped widgets. Widgets will be remapped per file rule type. - 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) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) - - @QtCore.Slot(int) - def _on_current_row_changed(self, row: int) -> None: - if row != -1: - # Update parameter widget states, since file rule type may differ from - # the previous rule. - file_rule_type = self.model.data( - self.model.index(row, self.model.FILE_RULE_TYPE.column), - QtCore.Qt.EditRole, - ) - 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 deleted file mode 100644 index 170b1f69db..0000000000 --- a/src/apps/ocioview/ocioview/items/file_rule_model.py +++ /dev/null @@ -1,434 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import enum -from dataclasses import dataclass, field -from typing import Any, Optional, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..undo import ConfigSnapshotUndoCommand -from ..utils import get_glyph_icon, next_name -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -class FileRuleType(str, enum.Enum): - """Enum of file rule types.""" - - RULE_BASIC = "Basic Rule" - RULE_REGEX = "Regex Rule" - RULE_OCIO_V1 = "OCIO v1 Style Rule" - RULE_DEFAULT = "Default Rule" - - -@dataclass -class FileRule: - """Individual file rule storage.""" - - type: FileRuleType - name: str - color_space: str = "" - pattern: str = "" - regex: str = "" - extension: str = "" - custom_keys: dict[str, str] = field(default_factory=dict) - - def args(self) -> Union[tuple[str, str, str, str], tuple[str, str, str]]: - """ - Return tuple of *args for - ``FileRules.insertRule(index, *args)``, which will correspond - to a basic or regex rule, the two variations of this overloaded - function. - """ - if self.type == FileRuleType.RULE_REGEX: - return self.name, self.color_space, self.regex - else: # FileRuleType.RULE_BASIC: - return self.name, self.color_space, self.pattern, self.extension - - -class FileRuleModel(BaseConfigItemModel): - """ - Item model for editing file rules in the current config. - """ - - FILE_RULE_TYPE = ColumnDesc(0, "File Rule Type", str) - NAME = ColumnDesc(1, "Name", str) - COLOR_SPACE = ColumnDesc(2, "Color Space", str) - PATTERN = ColumnDesc(3, "Pattern", str) - REGEX = ColumnDesc(4, "Regex", str) - EXTENSION = ColumnDesc(5, "Extension", str) - CUSTOM_KEYS = ColumnDesc(6, "Custom Keys", list) - - COLUMNS = sorted( - [ - FILE_RULE_TYPE, - NAME, - COLOR_SPACE, - PATTERN, - REGEX, - EXTENSION, - CUSTOM_KEYS, - ], - key=lambda s: s.column, - ) - - __item_type__ = FileRule - __icon_glyph__ = "mdi6.file-check-outline" - - @classmethod - def get_rule_type_icon(cls, rule_type: FileRuleType) -> QtGui.QIcon: - glyph_names = { - FileRuleType.RULE_BASIC: "ph.asterisk", - FileRuleType.RULE_REGEX: "msc.regex", - FileRuleType.RULE_OCIO_V1: "mdi6.contain", - FileRuleType.RULE_DEFAULT: "ph.arrow-line-down", - } - return get_glyph_icon(glyph_names[rule_type], size=ICON_SIZE_ITEM) - - @classmethod - def has_presets(cls) -> bool: - return True - - @classmethod - def requires_presets(cls) -> bool: - return True - - @classmethod - def get_presets(cls) -> Optional[Union[list[str], dict[str, QtGui.QIcon]]]: - return { - FileRuleType.RULE_BASIC.value: cls.get_rule_type_icon( - FileRuleType.RULE_BASIC - ), - FileRuleType.RULE_REGEX.value: cls.get_rule_type_icon( - FileRuleType.RULE_REGEX - ), - FileRuleType.RULE_OCIO_V1.value: cls.get_rule_type_icon( - FileRuleType.RULE_OCIO_V1 - ), - } - - 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_OCIO_V1: self.get_rule_type_icon( - FileRuleType.RULE_OCIO_V1 - ), - FileRuleType.RULE_DEFAULT: self.get_rule_type_icon( - FileRuleType.RULE_DEFAULT - ), - } - - ConfigCache.register_reset_callback(self._reset_cache) - - def add_preset(self, preset_name: str) -> int: - file_rules = self._get_editable_file_rules() - all_names = self.get_item_names() - - if preset_name == FileRuleType.RULE_BASIC.value: - item = FileRule( - FileRuleType.RULE_BASIC, - next_name("BasicRule_", all_names), - ConfigCache.get_default_color_space_name(), - pattern="*", - extension="*", - ) - elif preset_name == FileRuleType.RULE_REGEX.value: - item = FileRule( - FileRuleType.RULE_REGEX, - next_name("RegexRule_", all_names), - ConfigCache.get_default_color_space_name(), - regex=".*", - ) - else: # FileRuleType.RULE_OCIO_V1.value - # Only one instance of this rule is allowed - if ocio.FILE_PATH_SEARCH_RULE_NAME not in all_names: - item = FileRule( - FileRuleType.RULE_OCIO_V1, - ocio.FILE_PATH_SEARCH_RULE_NAME, - ConfigCache.get_default_color_space_name(), - ) - else: - return file_rules.getIndexForRule( - ocio.FILE_PATH_SEARCH_RULE_NAME - ) - - # Make new rule top priority - row = 0 - - with ConfigSnapshotUndoCommand( - f"Add {self.item_type_label()}", model=self, item_name=item.name - ): - self.beginInsertRows(self.NULL_INDEX, row, row) - self._insert_rule(row, file_rules, item) - - ocio.GetCurrentConfig().setFileRules(file_rules) - - self.endInsertRows() - self.item_added.emit(item.name) - - return row - - def move_item_up(self, item_name: str) -> bool: - """ - Increase priority (index - 1) for the named rule. - """ - if item_name != ocio.DEFAULT_RULE_NAME: - file_rules = self._get_editable_file_rules() - src_rule_index = file_rules.getIndexForRule(item_name) - dst_rule_index = max(0, src_rule_index - 1) - - if src_rule_index != dst_rule_index: - if not self.beginMoveRows( - QtCore.QModelIndex(), - src_rule_index, - src_rule_index, - QtCore.QModelIndex(), - dst_rule_index, - ): - return False - - with ConfigSnapshotUndoCommand( - f"Move {self.item_type_label()}", - model=self, - item_name=item_name, - ): - file_rules.increaseRulePriority(src_rule_index) - ocio.GetCurrentConfig().setFileRules(file_rules) - - self.endMoveRows() - self.item_moved.emit() - - return True - - return False - - def move_item_down(self, item_name: str) -> bool: - """ - Decrease priority (index + 1) for the named rule. - """ - if item_name != ocio.DEFAULT_RULE_NAME: - file_rules = self._get_editable_file_rules() - rule_count = file_rules.getNumEntries() - src_rule_index = file_rules.getIndexForRule(item_name) - dst_rule_index = min(rule_count - 2, src_rule_index + 1) - - if src_rule_index != dst_rule_index: - if not self.beginMoveRows( - QtCore.QModelIndex(), - src_rule_index, - src_rule_index, - QtCore.QModelIndex(), - dst_rule_index + 1, - ): - return False - - with ConfigSnapshotUndoCommand( - f"Move {self.item_type_label()}", - model=self, - item_name=item_name, - ): - file_rules.decreaseRulePriority(src_rule_index) - ocio.GetCurrentConfig().setFileRules(file_rules) - - self.endMoveRows() - self.item_moved.emit() - - return True - - return False - - 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()) - ] - - def _get_icon( - self, item: FileRule, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - if column_desc == self.NAME: - return self._rule_type_icons[item.type] - else: - return None - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[FileRule]: - if ConfigCache.validate() and self._items: - return self._items - - config = ocio.GetCurrentConfig() - file_rules = config.getFileRules() - self._items = [] - - for i in range(file_rules.getNumEntries()): - name = file_rules.getName(i) - color_space = file_rules.getColorSpace(i) - pattern = file_rules.getPattern(i) - regex = file_rules.getRegex(i) - extension = file_rules.getExtension(i) - - if name == ocio.DEFAULT_RULE_NAME: - file_rule_type = FileRuleType.RULE_DEFAULT - elif name == ocio.FILE_PATH_SEARCH_RULE_NAME: - file_rule_type = FileRuleType.RULE_OCIO_V1 - elif regex: - file_rule_type = FileRuleType.RULE_REGEX - else: - file_rule_type = FileRuleType.RULE_BASIC - - custom_keys = {} - for j in range(file_rules.getNumCustomKeys(i)): - key_name = file_rules.getCustomKeyName(i, j) - key_value = file_rules.getCustomKeyValue(i, j) - custom_keys[key_name] = key_value - - self._items.append( - FileRule( - file_rule_type, - name, - color_space, - pattern, - regex, - extension, - custom_keys, - ) - ) - - return self._items - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().setFileRules(ocio.FileRules()) - - @staticmethod - def _insert_rule( - index: int, file_rules: ocio.FileRules, item: FileRule - ) -> None: - """ - Insert rule into an ``ocio.FileRules`` object from a FileRule - instance. - """ - if item.name == ocio.FILE_PATH_SEARCH_RULE_NAME: - file_rules.insertPathSearchRule(index) - elif item.name == ocio.DEFAULT_RULE_NAME: - file_rules.setDefaultRuleColorSpace(item.color_space) - else: - file_rules.insertRule(index, *item.args()) - 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: - """Remove existing rule with name matching the provided rule.""" - # Default rule can't be removed - if item.name != ocio.DEFAULT_RULE_NAME: - for i in range(file_rules.getNumEntries()): - if file_rules.getName(i) == item.name: - file_rules.removeRule(i) - break - - def _get_editable_file_rules(self) -> ocio.FileRules: - """ - Copy existing config rules into new editable ``ocio.FileRules`` - instance. - """ - file_rules = ocio.FileRules() - for i, item in enumerate(self._get_items()): - self._insert_rule(i, file_rules, item) - return file_rules - - def _add_item(self, item: FileRule) -> None: - # Only presets can be added - pass - - def _remove_item(self, item: FileRule) -> None: - file_rules = self._get_editable_file_rules() - self._remove_named_rule(file_rules, item) - ocio.GetCurrentConfig().setFileRules(file_rules) - - def _new_item(self, name: str) -> None: - # Only presets can be added - pass - - def _get_value(self, item: FileRule, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.FILE_RULE_TYPE: - return item.type - if column_desc == self.NAME: - return item.name - elif column_desc == self.COLOR_SPACE: - return item.color_space - elif column_desc == self.PATTERN: - return item.pattern - elif column_desc == self.REGEX: - return item.regex - elif column_desc == self.EXTENSION: - return item.extension - elif column_desc == self.CUSTOM_KEYS: - return list(item.custom_keys.items()) - - # Invalid column - return None - - def _set_value( - self, - item: FileRule, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - file_rules = self._get_editable_file_rules() - current_index = file_rules.getIndexForRule(item.name) - prev_item_name = item.name - - # Update parameters - if column_desc == self.NAME: - if item.type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX): - # Name must be unique - if value not in self.get_item_names(): - # Remove rule with previous name before adding new one - self._remove_named_rule(file_rules, item) - item.name = value - self._insert_rule(current_index, file_rules, item) - - elif column_desc == self.COLOR_SPACE: - if item.type != FileRuleType.RULE_OCIO_V1: - file_rules.setColorSpace(current_index, value) - elif column_desc == self.PATTERN: - if item.type == FileRuleType.RULE_BASIC: - file_rules.setPattern(current_index, value) - elif column_desc == self.REGEX: - if item.type == FileRuleType.RULE_REGEX: - file_rules.setRegex(current_index, value) - elif column_desc == self.EXTENSION: - if item.type == FileRuleType.RULE_BASIC: - file_rules.setExtension(current_index, value) - - elif column_desc == self.CUSTOM_KEYS: - if item.type in (FileRuleType.RULE_BASIC, FileRuleType.RULE_REGEX): - item.custom_keys.clear() - for key_name, key_value in value: - item.custom_keys[key_name] = key_value - - # Need to re-add rule to replace custom keys - self._remove_named_rule(file_rules, item) - self._insert_rule(current_index, file_rules, item) - - ocio.GetCurrentConfig().setFileRules(file_rules) - - if item.name != prev_item_name: - self.item_renamed.emit(item.name, prev_item_name) diff --git a/src/apps/ocioview/ocioview/items/look_edit.py b/src/apps/ocioview/ocioview/items/look_edit.py deleted file mode 100644 index d815be1811..0000000000 --- a/src/apps/ocioview/ocioview/items/look_edit.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtWidgets - -from ..widgets import ColorSpaceComboBox, TextEdit -from .look_model import LookModel -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit - - -class LookParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing the parameters and transforms for one look. - """ - - __model_type__ = LookModel - __has_transforms__ = True - __from_ref_column_desc__ = LookModel.TRANSFORM - __to_ref_column_desc__ = LookModel.INVERSE_TRANSFORM - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.process_space_combo = ColorSpaceComboBox( - ocio.SEARCH_REFERENCE_SPACE_SCENE, include_roles=True - ) - self.description_edit = TextEdit() - - # Layout - self._param_layout.addRow( - self.model.PROCESS_SPACE.label, self.process_space_combo - ) - self._param_layout.addRow( - self.model.DESCRIPTION.label, self.description_edit - ) - - -class LookEdit(BaseConfigItemEdit): - """ - Widget for editing all looks in the current config. - """ - - __param_edit_type__ = LookParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Map widgets to model columns - self.mapper.addMapping( - self.param_edit.process_space_combo, model.PROCESS_SPACE.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.process_space_combo.color_space_changed.connect( - partial(self.param_edit.submit_mapper_deferred, self.mapper) - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) diff --git a/src/apps/ocioview/ocioview/items/look_model.py b/src/apps/ocioview/ocioview/items/look_model.py deleted file mode 100644 index b2e19372dc..0000000000 --- a/src/apps/ocioview/ocioview/items/look_model.py +++ /dev/null @@ -1,224 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import copy -from typing import Any, Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..ref_space_manager import ReferenceSpaceManager -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -class LookModel(BaseConfigItemModel): - """ - Item model for editing looks in the current config. - """ - - NAME = ColumnDesc(0, "Name", str) - PROCESS_SPACE = ColumnDesc(1, "Process Space", str) - TRANSFORM = ColumnDesc(2, "Transform", ocio.Transform) - INVERSE_TRANSFORM = ColumnDesc(3, "Inverse Transform", ocio.Transform) - DESCRIPTION = ColumnDesc(4, "Description", str) - - COLUMNS = sorted( - [NAME, PROCESS_SPACE, TRANSFORM, INVERSE_TRANSFORM, DESCRIPTION], - key=lambda s: s.column, - ) - - __item_type__ = ocio.Look - __icon_glyph__ = "ri.clapperboard-line" - - def get_item_names(self) -> list[str]: - return [item.getName() for item in self._get_items()] - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get look name from subscription item label - item_name = self.extract_subscription_item_name(item_label) - - scene_ref_name = ( - ReferenceSpaceManager.scene_reference_space().getName() - ) - return ( - ocio.LookTransform( - src=scene_ref_name, - dst=scene_ref_name, - looks=item_name, - direction=ocio.TRANSFORM_DIR_FORWARD, - ), - ocio.LookTransform( - src=scene_ref_name, - dst=scene_ref_name, - looks=item_name, - direction=ocio.TRANSFORM_DIR_INVERSE, - ), - ) - - 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(item, column_desc) - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - if column_desc == self.NAME: - return self._get_subscription_color(item, column_desc) - else: - return None - - def _get_items(self, preserve: bool = False) -> list[ocio.Look]: - # TODO: Revert to using ConfigCache following fix of issue: - # https://github.com/AcademySoftwareFoundation/OpenColorIO/issues/1817 - config = ocio.GetCurrentConfig() - if preserve: - # self._items = [copy.deepcopy(item) for item in ConfigCache.get_looks()] - self._items = [copy.deepcopy(item) for item in config.getLooks()] - return self._items - else: - # return ConfigCache.get_looks() - return config.getLooks() - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().clearLooks() - - def _add_item(self, item: ocio.Look) -> None: - ocio.GetCurrentConfig().addLook(item) - - def _remove_item(self, item: ocio.Look) -> None: - config = ocio.GetCurrentConfig() - items = [ - copy.deepcopy(other_item) - for other_item in ConfigCache.get_looks() - if other_item != item - ] - - config.clearLooks() - - for other_item in items: - config.addLook(other_item) - - def _new_item(self, name: str) -> None: - config = ocio.GetCurrentConfig() - color_space = ConfigCache.get_default_color_space_name() - if not color_space: - color_spaces = ConfigCache.get_color_space_names() - if color_spaces: - color_space = color_spaces[0] - if color_space: - config.addLook(ocio.Look(name=name, processSpace=color_space)) - else: - config.addLook(ocio.Look(name=name)) - - def _get_value(self, item: ocio.Look, column_desc: ColumnDesc) -> Any: - config = ocio.GetCurrentConfig() - - # Get parameters - if column_desc == self.NAME: - return item.getName() - elif column_desc == self.DESCRIPTION: - return item.getDescription() - - # Process space (color space name) - elif column_desc == self.PROCESS_SPACE: - process_space = item.getProcessSpace() - if not process_space: - - # 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, - ): - process_space = config.getCanonicalName(role) - if process_space: - break - - if not process_space: - # Next look for any log-encoded color space. This probably isn't the - # right choice, but will start in the right direction. - for color_space in config.getColorSpaces( - ocio.SEARCH_REFERENCE_SPACE_SCENE, ocio.COLORSPACE_ALL - ): - if color_space.getEncoding() == "log": - process_space = color_space.getName() - break - - if not process_space: - # Lastly, get the first color space. Something is better than - # nothing. - color_space_name_iter = config.getColorSpaceNames( - ocio.SEARCH_REFERENCE_SPACE_SCENE, ocio.COLORSPACE_ALL - ) - try: - process_space = next(color_space_name_iter) - except StopIteration: - pass - - return process_space or "" - - # Get transforms - elif column_desc == self.TRANSFORM: - return item.getTransform() - elif column_desc == self.INVERSE_TRANSFORM: - return item.getInverseTransform() - - # Invalid column - return None - - def _set_value( - self, - item: ocio.Look, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - config = ocio.GetCurrentConfig() - new_item = copy.deepcopy(item) - prev_item_name = item.getName() - - # Update parameters - if column_desc == self.NAME: - new_item.setName(value) - elif column_desc == self.PROCESS_SPACE: - new_item.setProcessSpace(value) - elif column_desc == self.DESCRIPTION: - new_item.setDescription(value) - - # Update transforms - if column_desc == self.TRANSFORM: - new_item.setTransform(value) - elif column_desc == self.INVERSE_TRANSFORM: - new_item.setInverseTransform(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() - ] - config.clearLooks() - for other_look in items: - if other_look.getName() == prev_item_name: - config.addLook(new_item) - self.item_renamed.emit(new_item.getName(), prev_item_name) - else: - config.addLook(other_look) - - # Item order is preserved for all other changes - else: - config.addLook(new_item) - - # Broadcast transform or name changes to subscribers - 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, - ) diff --git a/src/apps/ocioview/ocioview/items/named_transform_edit.py b/src/apps/ocioview/ocioview/items/named_transform_edit.py deleted file mode 100644 index b70d9919af..0000000000 --- a/src/apps/ocioview/ocioview/items/named_transform_edit.py +++ /dev/null @@ -1,115 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtWidgets - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from ..widgets import CallbackComboBox, StringListWidget, TextEdit -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit -from .named_transform_model import NamedTransformModel - - -class NamedTransformParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing the parameters and transforms for one named - transform. - """ - - __model_type__ = NamedTransformModel - __has_transforms__ = True - __from_ref_column_desc__ = NamedTransformModel.FORWARD_TRANSFORM - __to_ref_column_desc__ = NamedTransformModel.INVERSE_TRANSFORM - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.aliases_list = StringListWidget( - item_basename="alias", - item_icon=get_glyph_icon( - "ph.bookmark-simple", size=ICON_SIZE_ITEM - ), - ) - self.family_edit = CallbackComboBox( - ConfigCache.get_families, editable=True - ) - self.encoding_edit = CallbackComboBox( - ConfigCache.get_encodings, editable=True - ) - self.description_edit = TextEdit() - self.categories_list = StringListWidget( - item_basename="category", - item_icon=get_glyph_icon( - "ph.bookmarks-simple", 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 - ) - - def _get_available_categories(self) -> list[str]: - """ - :return: All unused categories which can be added as presets - to this item. - """ - current_categories = self.categories_list.items() - return [ - c - for c in ConfigCache.get_categories() - if c not in current_categories - ] - - -class NamedTransformEdit(BaseConfigItemEdit): - """ - Widget for editing all named transforms in the current config. - """ - - __param_edit_type__ = NamedTransformParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - 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.param_edit.description_edit, model.DESCRIPTION.column - ) - 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 - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) diff --git a/src/apps/ocioview/ocioview/items/named_transform_model.py b/src/apps/ocioview/ocioview/items/named_transform_model.py deleted file mode 100644 index 814e1ec854..0000000000 --- a/src/apps/ocioview/ocioview/items/named_transform_model.py +++ /dev/null @@ -1,230 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import copy -from typing import Any, Optional, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -class NamedTransformModel(BaseConfigItemModel): - """ - Item model for editing named transforms in the current config. - """ - - NAME = ColumnDesc(0, "Name", str) - ALIASES = ColumnDesc(1, "Aliases", list) - FAMILY = ColumnDesc(2, "Family", str) - ENCODING = ColumnDesc(3, "Encoding", str) - DESCRIPTION = ColumnDesc(4, "Description", str) - CATEGORIES = ColumnDesc(5, "Categories", list) - FORWARD_TRANSFORM = ColumnDesc(6, "Forward Transform", ocio.Transform) - INVERSE_TRANSFORM = ColumnDesc(7, "Inverse Transform", ocio.Transform) - - # fmt: off - COLUMNS = sorted([ - NAME, ALIASES, FAMILY, ENCODING, DESCRIPTION, CATEGORIES, - FORWARD_TRANSFORM, INVERSE_TRANSFORM, - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = ocio.NamedTransform - __icon_glyph__ = "ph.arrow-square-right" - - 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 - ) - - def get_item_names(self) -> list[str]: - return [item.getName() for item in self._get_items()] - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get named transform name from subscription item label - item_name = self.extract_subscription_item_name(item_label) - - config = ocio.GetCurrentConfig() - named_transform = config.getNamedTransform(item_name) - if named_transform is not None: - - fwd_tf = named_transform.getTransform(ocio.TRANSFORM_DIR_FORWARD) - if not fwd_tf: - inv_tf_ = named_transform.getTransform( - ocio.TRANSFORM_DIR_INVERSE - ) - if inv_tf_: - 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 - ) - if fwd_tf_: - inv_tf = ocio.GroupTransform( - [fwd_tf_], ocio.TRANSFORM_DIR_INVERSE - ) - - return fwd_tf, inv_tf - else: - return None, None - - 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(item, column_desc) - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - if column_desc == self.NAME: - return self._get_subscription_color(item, column_desc) - else: - return None - - def _get_items(self, preserve: bool = False) -> list[ocio.ColorSpace]: - if preserve: - self._items = [ - copy.deepcopy(item) - for item in ConfigCache.get_named_transforms() - ] - return self._items - else: - return ConfigCache.get_named_transforms() - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().clearNamedTransforms() - - def _add_item(self, item: ocio.NamedTransform) -> None: - ocio.GetCurrentConfig().addNamedTransform(item) - - def _remove_item(self, item: ocio.NamedTransform) -> None: - config = ocio.GetCurrentConfig() - items = [ - copy.deepcopy(other_item) - for other_item in ConfigCache.get_named_transforms() - if other_item != item - ] - - config.clearNamedTransforms() - - for other_item in items: - config.addNamedTransform(other_item) - - def _new_item(self, name: str) -> None: - ocio.GetCurrentConfig().addNamedTransform( - ocio.NamedTransform( - name=name, forwardTransform=ocio.GroupTransform() - ) - ) - - def _get_value( - self, item: ocio.NamedTransform, column_desc: ColumnDesc - ) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.getName() - elif column_desc == self.ALIASES: - return list(item.getAliases()) - elif column_desc == self.FAMILY: - return item.getFamily() - elif column_desc == self.ENCODING: - return item.getEncoding() - elif column_desc == self.DESCRIPTION: - return item.getDescription() - elif column_desc == self.CATEGORIES: - return list(item.getCategories()) - - # Get transforms - elif column_desc in (self.FORWARD_TRANSFORM, self.INVERSE_TRANSFORM): - return item.getTransform( - ocio.TRANSFORM_DIR_FORWARD - if column_desc == self.FORWARD_TRANSFORM - else ocio.TRANSFORM_DIR_INVERSE - ) - - # Invalid column - return None - - def _set_value( - self, - item: ocio.NamedTransform, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - config = ocio.GetCurrentConfig() - new_item = copy.deepcopy(item) - prev_item_name = item.getName() - - # Update parameters - if column_desc == self.NAME: - new_item.setName(value) - elif column_desc == self.ALIASES: - new_item.clearAliases() - for alias in value: - new_item.addAlias(alias) - elif column_desc == self.FAMILY: - new_item.setFamily(value) - elif column_desc == self.ENCODING: - new_item.setEncoding(value) - elif column_desc == self.DESCRIPTION: - new_item.setDescription(value) - elif column_desc == self.CATEGORIES: - new_item.clearCategories() - for category in value: - new_item.addCategory(category) - - # Update transforms - elif column_desc in (self.FORWARD_TRANSFORM, self.INVERSE_TRANSFORM): - new_item.setTransform( - value, - ocio.TRANSFORM_DIR_FORWARD - if column_desc == self.FORWARD_TRANSFORM - else ocio.TRANSFORM_DIR_INVERSE, - ) - - # 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 ConfigCache.get_named_transforms() - ] - config.clearNamedTransforms() - for other_item in items: - if other_item.getName() == prev_item_name: - config.addNamedTransform(new_item) - self.item_renamed.emit(new_item.getName(), prev_item_name) - else: - config.addNamedTransform(other_item) - - # Item order is preserved for all other changes - else: - config.addNamedTransform(new_item) - - # Broadcast transform or name changes to subscribers - 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, - ) diff --git a/src/apps/ocioview/ocioview/items/role_edit.py b/src/apps/ocioview/ocioview/items/role_edit.py deleted file mode 100644 index 56593de78c..0000000000 --- a/src/apps/ocioview/ocioview/items/role_edit.py +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtGui, QtWidgets - -from ..signal_router import SignalRouter -from ..widgets import ItemModelTableWidget -from .delegates import RoleDelegate -from .role_model import RoleModel - - -class RoleEdit(QtWidgets.QWidget): - """ - Widget for editing all color space roles in the current config. - """ - - @classmethod - def item_type_icon(cls) -> QtGui.QIcon: - """ - :return: Item type icon - """ - return RoleModel.item_type_icon() - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - """ - :param plural: Whether label should be plural - :return: Friendly type name - """ - return RoleModel.item_type_label(plural=plural) - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - 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)) - - # Layout - tab_layout = QtWidgets.QVBoxLayout() - tab_layout.addWidget(self.table) - tab_frame = QtWidgets.QFrame() - tab_frame.setLayout(tab_layout) - - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab( - tab_frame, - RoleModel.item_type_icon(), - RoleModel.item_type_label(plural=True), - ) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.tabs) - self.setLayout(layout) diff --git a/src/apps/ocioview/ocioview/items/role_model.py b/src/apps/ocioview/ocioview/items/role_model.py deleted file mode 100644 index fb5f78bbf2..0000000000 --- a/src/apps/ocioview/ocioview/items/role_model.py +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from dataclasses import dataclass -from typing import Any - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..config_cache import ConfigCache -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -@dataclass -class Role: - """Individual role storage.""" - - name: str - color_space: str - - -class RoleModel(BaseConfigItemModel): - """ - Item model for color space roles in the current config. - """ - - NAME = ColumnDesc(0, "Name", str) - COLOR_SPACE = ColumnDesc(1, "Color Space", str) - - COLUMNS = [NAME, COLOR_SPACE] - - __item_type__ = Role - __icon_glyph__ = "ph.tag" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - ConfigCache.register_reset_callback(self._reset_cache) - - def flags(self, index: QtCore.QModelIndex) -> int: - return ( - QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsEditable - ) - - def get_item_names(self) -> list[str]: - return [item.name for item in self._get_items()] - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[Role]: - if ConfigCache.validate() and self._items: - return self._items - - self._items = [Role(*role) for role in ocio.GetCurrentConfig().getRoles()] - return self._items - - def _clear_items(self) -> None: - config = ocio.GetCurrentConfig() - for name in config.getRoleNames(): - # Unset role - config.setRole(name, None) - - def _add_item(self, item: Role) -> None: - ocio.GetCurrentConfig().setRole(item.name, item.color_space) - - def _remove_item(self, item: Role) -> None: - # Unset role - ocio.GetCurrentConfig().setRole(item.name, None) - - def _new_item(self, name: str) -> None: - color_space_names = ConfigCache.get_color_space_names( - ocio.SEARCH_REFERENCE_SPACE_SCENE - ) - if color_space_names: - ocio.GetCurrentConfig().setRole(name, color_space_names[0]) - - def _get_value(self, item: Role, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.name - elif column_desc == self.COLOR_SPACE: - return item.color_space - - # Invalid column - return None - - def _set_value( - self, item: Role, column_desc: ColumnDesc, value: Any, index: QtCore.QModelIndex - ) -> None: - config = ocio.GetCurrentConfig() - prev_item_name = item.name - - # Update parameters - if column_desc == self.NAME: - # Unset previous role name - config.setRole(prev_item_name, None) - item.name = value - elif column_desc == self.COLOR_SPACE: - item.color_space = value - - config.setRole(item.name, item.color_space) - - if item.name != prev_item_name: - # Tell views to follow selection to new item - self.item_added.emit(item.name) - - self.item_renamed.emit(item.name, prev_item_name) diff --git a/src/apps/ocioview/ocioview/items/rule_edit.py b/src/apps/ocioview/ocioview/items/rule_edit.py deleted file mode 100644 index bd759711c2..0000000000 --- a/src/apps/ocioview/ocioview/items/rule_edit.py +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from .file_rule_edit import FileRuleEdit -from .utils import adapt_splitter_sizes -from .viewing_rule_edit import ViewingRuleEdit - - -class RuleEdit(QtWidgets.QWidget): - """ - Widget for editing all file and viewing rules in the current - config. - """ - - @classmethod - def item_type_icon(cls) -> QtGui.QIcon: - """ - :return: Item type icon - """ - return get_glyph_icon("mdi6.list-status", size=ICON_SIZE_ITEM) - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - """ - :param plural: Whether label should be plural - :return: Friendly type name - """ - label = "Rule" - if plural: - label += "s" - return label - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.file_rule_edit = FileRuleEdit() - self.viewing_rule_edit = ViewingRuleEdit() - - # Layout - self.tabs = QtWidgets.QTabWidget() - self.tabs.addTab( - self.file_rule_edit, - self.file_rule_edit.item_type_icon(), - self.file_rule_edit.item_type_label(plural=True), - ) - self.tabs.addTab( - self.viewing_rule_edit, - self.viewing_rule_edit.item_type_icon(), - self.viewing_rule_edit.item_type_label(plural=True), - ) - self.tabs.currentChanged.connect(self._on_current_changed) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.tabs) - self.setLayout(layout) - - @property - def splitter(self) -> QtWidgets.QSplitter: - return self.tabs.currentWidget().splitter - - def set_splitter_sizes(self, from_sizes: list[int]) -> None: - """ - Update splitter to match the provided sizes. - - :param from_sizes: Sizes to match, with emphasis on matching - the first index. - """ - to_widget = self.tabs.currentWidget() - to_widget.splitter.setSizes( - adapt_splitter_sizes(from_sizes, to_widget.splitter.sizes()) - ) - - @QtCore.Slot(int) - def _on_current_changed(self, index: int) -> None: - """Match tab splitter sizes on tab change.""" - from_widget = self.tabs.widget(1 - index) - to_widget = self.tabs.widget(index) - - to_widget.splitter.setSizes( - adapt_splitter_sizes( - from_widget.splitter.sizes(), to_widget.splitter.sizes() - ) - ) diff --git a/src/apps/ocioview/ocioview/items/shared_view_edit.py b/src/apps/ocioview/ocioview/items/shared_view_edit.py deleted file mode 100644 index 1f93bf0644..0000000000 --- a/src/apps/ocioview/ocioview/items/shared_view_edit.py +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtWidgets - -from ..config_cache import ConfigCache -from ..widgets import CallbackComboBox, ColorSpaceComboBox, LineEdit -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit -from .shared_view_model import SharedViewModel - - -class SharedViewParamEdit(BaseConfigItemParamEdit): - """Widget for editing the parameters for a shared view.""" - - __model_type__ = SharedViewModel - __has_transforms__ = False - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - 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( - 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.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 - ) - - -class SharedViewEdit(BaseConfigItemEdit): - """ - Widget for editing all displays and views in the current config. - """ - - __param_edit_type__ = SharedViewParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Map widgets to model columns - self.mapper.addMapping( - self.param_edit.color_space_combo, model.COLOR_SPACE.column - ) - 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.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.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) - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) - - def set_current_view(self, view: str) -> None: - """Set the current shared view.""" - self.list.set_current_item(view) diff --git a/src/apps/ocioview/ocioview/items/shared_view_model.py b/src/apps/ocioview/ocioview/items/shared_view_model.py deleted file mode 100644 index e6ff4063a4..0000000000 --- a/src/apps/ocioview/ocioview/items/shared_view_model.py +++ /dev/null @@ -1,213 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from collections import defaultdict -from dataclasses import dataclass, field -from typing import Any, Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from .config_item_model import ColumnDesc, BaseConfigItemModel -from .display_model import View, DisplayModel -from .utils import ViewType - - -@dataclass -class SharedView(View): - """Individual shared view storage.""" - - displays: set[str] = field(default_factory=set) - - -class SharedViewModel(BaseConfigItemModel): - """ - Item model for editing shared views in the current config. - """ - - NAME = ColumnDesc(0, "View", str) - COLOR_SPACE = ColumnDesc(1, "Color Space", str) - VIEW_TRANSFORM = ColumnDesc(2, "View Transform", str) - LOOKS = ColumnDesc(3, "Looks", str) - RULE = ColumnDesc(4, "Rule", str) - DESCRIPTION = ColumnDesc(5, "Description", str) - - # fmt: off - COLUMNS = sorted([ - NAME, COLOR_SPACE, VIEW_TRANSFORM, LOOKS, RULE, DESCRIPTION - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = SharedView - __icon_glyph__ = "ph.share-network-bold" - - @classmethod - def get_view_type_icon(cls, view_type: ViewType) -> QtGui.QIcon: - return DisplayModel.get_view_type_icon(view_type) - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - ConfigCache.register_reset_callback(self._reset_cache) - - def get_item_names(self) -> list[str]: - return [v.name for v in self._get_items()] - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[SharedView]: - if ConfigCache.validate() and self._items: - return self._items - - config = ocio.GetCurrentConfig() - - shared_view_display_map = defaultdict(set) - for display in ConfigCache.get_displays(): - for view in ConfigCache.get_views( - display, view_type=ocio.VIEW_SHARED - ): - shared_view_display_map[view].add(display) - - self._items = [] - - for name in ConfigCache.get_shared_views(): - self._items.append( - SharedView( - ViewType.VIEW_SHARED, - name, - config.getDisplayViewColorSpaceName("", name), - config.getDisplayViewTransformName("", name), - config.getDisplayViewLooks("", name), - config.getDisplayViewRule("", name), - config.getDisplayViewDescription("", name), - shared_view_display_map[name], - ) - ) - - return self._items - - def _clear_items(self) -> None: - config = ocio.GetCurrentConfig() - for item in self._get_items(): - self._remove_item(item, config=config) - - def _add_item(self, item: SharedView) -> None: - config = ocio.GetCurrentConfig() - config.addSharedView( - item.name, - item.view_transform, - item.color_space, - item.looks, - item.rule, - item.description, - ) - for display in item.displays: - config.addDisplaySharedView(display, item.name) - - def _remove_item( - self, item: SharedView, config: Optional[ocio.Config] = None - ) -> None: - if config is None: - config = ocio.GetCurrentConfig() - - # Remove reference from all displays - for display in item.displays: - config.removeDisplayView(display, item.name) - - # Remove shared view - config.removeSharedView(item.name) - - def _new_item(self, name: str) -> None: - view_transform = ConfigCache.get_default_view_transform_name() - if not view_transform: - view_transforms = ConfigCache.get_view_transforms() - if view_transforms: - view_transform = view_transforms[0] - if not view_transform: - self.warning_raised.emit( - f"Could not create {self.item_type_label().lower()} because no view " - f"transforms are defined." - ) - return - - self._add_item( - SharedView( - ViewType.VIEW_SHARED, - name, - ocio.OCIO_VIEW_USE_DISPLAY_NAME, - view_transform, - ) - ) - - def _get_value(self, item: SharedView, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.NAME: - return item.name - elif column_desc == self.COLOR_SPACE: - return item.color_space - elif column_desc == self.VIEW_TRANSFORM: - return item.view_transform - elif column_desc == self.LOOKS: - return item.looks - elif column_desc == self.RULE: - return item.rule - elif column_desc == self.DESCRIPTION: - return item.description - - # Invalid column - return None - - def _set_value( - self, - item: SharedView, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - item_names = self.get_item_names() - if item.name not in item_names: - return - - config = ocio.GetCurrentConfig() - prev_item_name = item.name - items = self._get_items() - item_index = item_names.index(item.name) - - self._clear_items() - - # Update parameters - if column_desc == self.COLOR_SPACE: - is_valid = True - if value != ocio.OCIO_VIEW_USE_DISPLAY_NAME: - color_space = config.getColorSpace(value) - if ( - color_space is None - or color_space.getReferenceSpaceType() - != ocio.REFERENCE_SPACE_DISPLAY - ): - is_valid = False - - if is_valid: - items[item_index].color_space = value - - elif column_desc == self.VIEW_TRANSFORM: - items[item_index].view_transform = value - elif column_desc == self.NAME: - items[item_index].name = value - elif column_desc == self.LOOKS: - items[item_index].looks = value - elif column_desc == self.RULE: - items[item_index].rule = value - elif column_desc == self.DESCRIPTION: - items[item_index].description = value - - for other_item in items: - self._add_item(other_item) - - if item.name != prev_item_name: - # Tell views to follow selection to new item - self.item_added.emit(item.name) - - self.item_renamed.emit(item.name, prev_item_name) diff --git a/src/apps/ocioview/ocioview/items/utils.py b/src/apps/ocioview/ocioview/items/utils.py deleted file mode 100644 index 6beb6dde17..0000000000 --- a/src/apps/ocioview/ocioview/items/utils.py +++ /dev/null @@ -1,178 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -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.""" - - VIEW_SHARED = "Shared View" - VIEW_DISPLAY = "View (Display Reference Space)" - VIEW_SCENE = "View (Scene Reference Space)" - - -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: View type - """ - if not display: - return ViewType.VIEW_SHARED - - config = ocio.GetCurrentConfig() - - color_space_name = config.getDisplayViewColorSpaceName(display, view) - view_transform_name = config.getDisplayViewTransformName(display, view) - - color_space = config.getColorSpace(color_space_name) - if color_space is not None: - if color_space.getReferenceSpaceType() == ocio.REFERENCE_SPACE_SCENE: - if view_transform_name: - logger.warning( - f"Invalid view '{display}/{view}' references a view transform " - 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_SCENE - else: - return ViewType.VIEW_DISPLAY - - elif view_transform_name: - return ViewType.VIEW_DISPLAY - else: - return ViewType.VIEW_SCENE - - -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 - and three splitter sections. - - :param from_sizes: Sizes to adapt to - :param to_sizes: Sizes to adjust - :return: Adapted sizes to apply to destination - """ - from_count = len(from_sizes) - to_count = len(to_sizes) - - # Assumes 2-3 splitter sections - if from_count == 3 and to_count == 2: - return [from_sizes[0], sum(to_sizes) - from_sizes[0]] - elif from_count == 2 and to_count == 3: - return [ - from_sizes[0], - to_sizes[1], - sum(to_sizes) - from_sizes[0] - to_sizes[1], - ] - elif from_count == to_count: - return from_sizes - else: - return to_sizes - - -def get_scene_to_display_transform( - view_transform: ocio.ViewTransform, -) -> Optional[ocio.Transform]: - """ - Extract a scene-to-display transform from a view transform - instance. - - :param view_transform: View transform instance - :return: Scene to display transform, if available - """ - # REFERENCE_SPACE_SCENE - if view_transform.getReferenceSpaceType() == ocio.REFERENCE_SPACE_SCENE: - scene_to_display_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ) - if not scene_to_display_ref_tf: - display_to_scene_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - ) - if display_to_scene_ref_tf: - scene_to_display_ref_tf = ocio.GroupTransform( - [display_to_scene_ref_tf], ocio.TRANSFORM_DIR_INVERSE - ) - - # REFERENCE_SPACE_DISPLAY - else: - scene_to_display_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - ) - if not scene_to_display_ref_tf: - display_to_scene_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ) - if display_to_scene_ref_tf: - scene_to_display_ref_tf = ocio.GroupTransform( - [display_to_scene_ref_tf], ocio.TRANSFORM_DIR_INVERSE - ) - - # Has transform? - if scene_to_display_ref_tf: - return scene_to_display_ref_tf - else: - return None - - -def get_display_to_scene_transform( - view_transform: ocio.ViewTransform, -) -> Optional[ocio.Transform]: - """ - Extract a display-to-scene transform from a view transform - instance. - - :param view_transform: View transform instance - :return: Display to scene transform, if available - """ - # REFERENCE_SPACE_DISPLAY - if view_transform.getReferenceSpaceType() == ocio.REFERENCE_SPACE_DISPLAY: - display_to_scene_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ) - if not display_to_scene_ref_tf: - scene_to_display_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - ) - if scene_to_display_ref_tf: - display_to_scene_ref_tf = ocio.GroupTransform( - [scene_to_display_ref_tf], ocio.TRANSFORM_DIR_INVERSE - ) - - # REFERENCE_SPACE_SCENE - else: - display_to_scene_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - ) - if not display_to_scene_ref_tf: - scene_to_display_ref_tf = view_transform.getTransform( - ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ) - if scene_to_display_ref_tf: - display_to_scene_ref_tf = ocio.GroupTransform( - [scene_to_display_ref_tf], ocio.TRANSFORM_DIR_INVERSE - ) - - # Has transform? - if display_to_scene_ref_tf: - return display_to_scene_ref_tf - else: - return None diff --git a/src/apps/ocioview/ocioview/items/view_edit.py b/src/apps/ocioview/ocioview/items/view_edit.py deleted file mode 100644 index da4ab9089a..0000000000 --- a/src/apps/ocioview/ocioview/items/view_edit.py +++ /dev/null @@ -1,378 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..config_cache import ConfigCache -from ..transform_manager import TransformManager -from ..widgets import ( - CallbackComboBox, - ColorSpaceComboBox, - ExpandingStackedWidget, - FormLayout, - ItemModelListWidget, - LineEdit, -) -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit -from .display_model import DisplayModel -from .view_model import ViewType, ViewModel - - -class ViewParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing the parameters for a display/view pair. - """ - - __model_type__ = ViewModel - __has_transforms__ = False - - VIEW_LAYERS = [ - None, - ViewType.VIEW_SHARED, - ViewType.VIEW_DISPLAY, - ViewType.VIEW_SCENE, - ] - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - self.display_model = DisplayModel() - - # Keep track of existing mapper connections to prevent duplicate signal/slot - # connections. - self._connected = {} - - # Build stack widget layers - self._param_stack = ExpandingStackedWidget() - - self.display_edits = {} - self.name_edits = {} - self.color_space_combos = {} - self.view_transform_combos = {} - self.looks_edits = {} - self.rule_combos = {} - self.description_edits = {} - - self.edit_shared_view_button = QtWidgets.QPushButton( - self.model.get_view_type_icon(ViewType.VIEW_SHARED), - "Edit Shared View", - ) - - for view_type in self.VIEW_LAYERS: - params_layout = FormLayout() - params_layout.setContentsMargins(0, 0, 0, 0) - - display_edit = LineEdit() - self.display_edits[view_type] = display_edit - params_layout.addRow(self.display_model.NAME.label, display_edit) - - if view_type in (ViewType.VIEW_DISPLAY, ViewType.VIEW_SCENE): - name_edit = LineEdit() - self.name_edits[view_type] = name_edit - params_layout.addRow(self.model.NAME.label, name_edit) - - if view_type == ViewType.VIEW_DISPLAY: - view_transform_combo = CallbackComboBox( - ConfigCache.get_view_transform_names - ) - 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: - color_space_combo = ColorSpaceComboBox( - reference_space_type=ocio.SEARCH_REFERENCE_SPACE_SCENE, - ) - else: # ViewType.VIEW_DISPLAY - color_space_combo = ColorSpaceComboBox( - reference_space_type=ocio.SEARCH_REFERENCE_SPACE_DISPLAY, - ) - self.color_space_combos[view_type] = 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 - ) - 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 - ) - - elif view_type == ViewType.VIEW_SHARED: - params_layout.addRow(self.edit_shared_view_button) - - params = QtWidgets.QFrame() - params.setLayout(params_layout) - self._param_stack.addWidget(params) - - self._param_layout.removeRow(0) - self._param_layout.addRow(self._param_stack) - - def update_available_params( - self, - display_mapper: QtWidgets.QDataWidgetMapper, - view_mapper: QtWidgets.QDataWidgetMapper, - view_type: Optional[ViewType] = None, - ) -> None: - """ - Enable the interface needed to edit the current display and - view. - """ - display_mapper.clearMapping() - view_mapper.clearMapping() - - # Track view mapper connections - if view_mapper not in self._connected: - self._connected[view_mapper] = [] - - self._param_stack.setCurrentIndex(self.VIEW_LAYERS.index(view_type)) - - display_mapper.addMapping( - self.display_edits[view_type], self.display_model.NAME.column - ) - - if view_type in (ViewType.VIEW_DISPLAY, ViewType.VIEW_SCENE): - 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 - ) - - # Trigger color space update before losing focus - if color_space_combo not in self._connected[view_mapper]: - 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 - ) - - if view_type == ViewType.VIEW_DISPLAY: - view_transform_combo = self.view_transform_combos[view_type] - view_mapper.addMapping( - view_transform_combo, - self.model.VIEW_TRANSFORM.column, - ) - - # Trigger view transform update before losing focus - if view_transform_combo not in self._connected[view_mapper]: - view_transform_combo.currentIndexChanged.connect( - partial(self.submit_mapper_deferred, view_mapper) - ) - self._connected[view_mapper].append(view_transform_combo) - - view_mapper.addMapping( - self.rule_combos[view_type], self.model.RULE.column - ) - view_mapper.addMapping( - self.description_edits[view_type], - self.model.DESCRIPTION.column, - ) - - -class ViewEdit(BaseConfigItemEdit): - """ - Widget for editing all displays and views in the current config. - """ - - shared_view_selection_requested = QtCore.Signal(str) - - __param_edit_type__ = ViewParamEdit - - @classmethod - def item_type_label(cls, plural: bool = False) -> str: - 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) - - # Try to preserve view choice through display changes - self._prev_view = None - - self.display_model = self.param_edit.display_model - - # Widgets - self.display_list = ItemModelListWidget( - self.display_model, - self.display_model.NAME.column, - 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_selection_requested.connect( - lambda index: self.display_list.set_current_row(index.row()) - ) - self.display_model.item_renamed.connect(self._on_display_renamed) - - # 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.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 - ) - - self.param_edit.edit_shared_view_button.released.connect( - self._on_edit_shared_view_button_clicked - ) - - # Clear default mapped widgets from view model. Widgets will be remapped per - # view type. - self.mapper.clearMapping() - - # Layout - self.splitter.insertWidget(0, self.display_list) - - # Initialize - if self.display_model.rowCount(): - self.display_list.set_current_row(0) - - def _get_view_type(self, row: int) -> Optional[ViewType]: - """ - :param row: Current view row - :return: Current view type, if a view is selected - """ - if row >= 0: - return self.model.data( - self.model.index(row, self.model.VIEW_TYPE.column), - QtCore.Qt.EditRole, - ) - else: - # No view available or selected - return None - - def _on_display_editing_finished(self) -> None: - """ - Workaround for a Qt bug where the QLineEdit editingFinished - 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()) - self.param_edit.display_edits[view_type].blockSignals(True) - self._display_mapper.submit() - self.param_edit.display_edits[view_type].blockSignals(False) - - @QtCore.Slot(str, str) - def _on_display_renamed(self, display: str, prev_display: str) -> None: - """ - Called when current display is renamed, to trigger subscription - update for all views. - - :param display: New display name - :param prev_display: Previous display name - """ - for i in range(self.model.rowCount()): - view_index = self.model.index(i, self.model.NAME.column) - item_label = self.model.format_subscription_item_label(view_index) - prev_item_label = self.model.format_subscription_item_label( - view_index, display=prev_display - ) - slot = TransformManager.get_subscription_slot( - self.model, prev_item_label - ) - if slot != -1: - TransformManager.set_subscription(slot, self.model, item_label) - - @QtCore.Slot(int) - def _on_display_changed(self, display_row: int) -> None: - """ - Called when current display is changed, to trigger view list - update. - - :param display_row: Current display row - """ - self.param_edit.setEnabled(display_row >= 0) - if display_row < 0: - self.param_edit.reset() - else: - # Get display and view names - display = self.display_model.data( - self.display_model.index( - display_row, self.display_model.NAME.column - ) - ) - - # Update view model - self.model.set_display(display) - - # Update display mapper - self._display_mapper.setCurrentIndex(display_row) - - # Update view list - view_row = -1 - if self.model.rowCount(): - view_row = 0 - if self._prev_view is not None: - selected, other_view_row = self.list.set_current_item( - self._prev_view - ) - if selected: - view_row = other_view_row - - self.list.set_current_row(view_row) - - def _on_edit_shared_view_button_clicked(self) -> None: - """Goto and select the current shared view.""" - view_index = self.list.current_index() - view = self.model.get_item_name(view_index) - if view: - self.shared_view_selection_requested.emit(view) - - @QtCore.Slot(int) - 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 - ) - ) - view_type = self.model.data( - self.model.index(view_row, self.model.VIEW_TYPE.column), - QtCore.Qt.EditRole, - ) - - # Update parameter widget states, since view type may have changed - self.param_edit.update_available_params( - self._display_mapper, self.mapper, view_type - ) - - # Update display params - 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 deleted file mode 100644 index 0af2350794..0000000000 --- a/src/apps/ocioview/ocioview/items/view_model.py +++ /dev/null @@ -1,563 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Any, Optional, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..ref_space_manager import ReferenceSpaceManager -from ..undo import ConfigSnapshotUndoCommand -from ..utils import get_glyph_icon, next_name -from .config_item_model import ColumnDesc, BaseConfigItemModel -from .display_model import View -from .utils import ViewType, get_view_type - - -class ViewModel(BaseConfigItemModel): - """ - Item model for editing display-defined views in the current config. - """ - - VIEW_TYPE = ColumnDesc(0, "View Type", str) - NAME = ColumnDesc(1, "View", str) - COLOR_SPACE = ColumnDesc(2, "Color Space", str) - VIEW_TRANSFORM = ColumnDesc(3, "View Transform", str) - LOOKS = ColumnDesc(4, "Looks", str) - RULE = ColumnDesc(5, "Rule", str) - DESCRIPTION = ColumnDesc(6, "Description", str) - - # fmt: off - COLUMNS = sorted([ - VIEW_TYPE, NAME, COLOR_SPACE, VIEW_TRANSFORM, LOOKS, RULE, DESCRIPTION - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = View - __icon_glyph__ = "mdi6.monitor-eye" - - @classmethod - def get_view_type_icon(cls, view_type: ViewType) -> QtGui.QIcon: - glyph_names = { - ViewType.VIEW_SHARED: "ph.share-network-bold", - ViewType.VIEW_DISPLAY: "mdi6.eye-outline", - ViewType.VIEW_SCENE: "mdi6.eye", - } - return get_glyph_icon(glyph_names[view_type], size=ICON_SIZE_ITEM) - - @classmethod - def has_presets(cls) -> bool: - return True - - @classmethod - def requires_presets(cls) -> bool: - return True - - @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_SCENE: cls.get_view_type_icon(ViewType.VIEW_SCENE), - } - for view in ConfigCache.get_shared_views(): - presets[view] = cls.get_view_type_icon(ViewType.VIEW_SHARED) - - return presets - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - 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_SCENE: self.get_view_type_icon(ViewType.VIEW_SCENE), - } - - ConfigCache.register_reset_callback(self._reset_cache) - - def set_display(self, display: str) -> None: - """ - :param display: Display to model views for - """ - self.beginResetModel() - - self._display = display - self._reset_cache() - - self.endResetModel() - - def add_preset(self, preset_name: str) -> int: - if not self._display: - return -1 - - item = None - - # Scene-referred view - if preset_name == ViewType.VIEW_SCENE.value: - color_spaces = ConfigCache.get_color_space_names( - ocio.SEARCH_REFERENCE_SPACE_SCENE - ) - if color_spaces: - color_space = color_spaces[0] - views = ConfigCache.get_views( - view_type=ocio.VIEW_DISPLAY_DEFINED - ) - - item = View( - ViewType.VIEW_SCENE, - next_name("View_", views), - color_space, - "", - ) - else: - self.warning_raised.emit( - f"Could not create {ViewType.VIEW_SCENE.value.lower()} because no " - f"color spaces with a scene reference type are defined." - ) - - # Display-referred view - elif preset_name == ViewType.VIEW_DISPLAY.value: - view_transform = ConfigCache.get_default_view_transform_name() - if not view_transform: - view_transforms = ConfigCache.get_view_transforms() - if view_transforms: - view_transform = view_transforms[0] - - if view_transform: - color_spaces = ConfigCache.get_color_space_names( - ocio.SEARCH_REFERENCE_SPACE_DISPLAY - ) - if color_spaces: - color_space = color_spaces[0] - views = ConfigCache.get_views( - view_type=ocio.VIEW_DISPLAY_DEFINED - ) - - item = View( - ViewType.VIEW_DISPLAY, - next_name("View_", views), - color_space, - view_transform, - ) - else: - self.warning_raised.emit( - f"Could not create {ViewType.VIEW_DISPLAY.value.lower()} " - f"because no color spaces with a display reference type are " - f"defined." - ) - else: - self.warning_raised.emit( - f"Could not create {ViewType.VIEW_DISPLAY.value.lower()} because " - f"no view transforms are defined." - ) - - # Shared view, which always follow display-defined views, as stored in the - # config YAML data. - else: - item = View(ViewType.VIEW_SHARED, preset_name, "", "") - - # Append new view to display - row = -1 - - if item is not None: - with ConfigSnapshotUndoCommand( - f"Add {self.item_type_label()}", - model=self, - item_name=item.name, - ): - self._add_item(item) - row = self.get_item_names().index(item.name) - - self.beginInsertRows(self.NULL_INDEX, row, row) - self.endInsertRows() - self.item_added.emit(item.name) - - return row - - def move_item_up(self, item_name: str) -> bool: - item_names = self.get_item_names() - if item_name not in item_names: - return False - - src_row = item_names.index(item_name) - - items = self._get_items() - item = items[src_row] - - if src_row > 0: - # Display-defined and shared views are kept separate, so we determine move - # capability only relative to similar view types (with scene and - # display-referred views being interchangeable). - prev_item = items[src_row - 1] - if ( - item.type != ViewType.VIEW_SHARED - and prev_item.type != ViewType.VIEW_SHARED - ) or ( - item.type == ViewType.VIEW_SHARED - and prev_item.type == ViewType.VIEW_SHARED - ): - dst_row = src_row - 1 - return self.moveRows( - self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row - ) - - return False - - def move_item_down(self, item_name: str) -> bool: - item_names = self.get_item_names() - if item_name not in item_names: - return False - - src_row = item_names.index(item_name) - - items = self._get_items() - item = items[src_row] - - if src_row < len(items) - 1: - # Display-defined and shared views are kept separate, so we determine move - # capability only relative to similar view types (with scene and - # display-referred views being interchangeable). - next_item = items[src_row + 1] - if ( - item.type != ViewType.VIEW_SHARED - and next_item.type != ViewType.VIEW_SHARED - ) or ( - item.type == ViewType.VIEW_SHARED - and next_item.type == ViewType.VIEW_SHARED - ): - dst_row = src_row + 1 - return self.moveRows( - self.NULL_INDEX, src_row, 1, self.NULL_INDEX, dst_row - ) - - return False - - def get_item_names(self) -> list[str]: - return [v.name for v in self._get_items()] - - def get_item(self, index: QtCore.QModelIndex) -> Optional[tuple[str, str]]: - if self._display: - items = self._get_items() - row = index.row() - if row < len(items): - return self._display, items[row].name - return None - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - - if self._display is not None: - # Get view name from subscription item label - item_name = self.extract_subscription_item_name(item_label) - - scene_ref_name = ( - ReferenceSpaceManager.scene_reference_space().getName() - ) - return ( - ocio.DisplayViewTransform( - src=scene_ref_name, - display=self._display, - view=item_name, - direction=ocio.TRANSFORM_DIR_FORWARD, - ), - ocio.DisplayViewTransform( - src=scene_ref_name, - display=self._display, - view=item_name, - direction=ocio.TRANSFORM_DIR_INVERSE, - ), - ) - else: - return None, None - - def format_subscription_item_label( - self, - item_name_or_index: Union[str, QtCore.QModelIndex], - display: Optional[str] = None, - **kwargs, - ) -> Optional[str]: - item_label = super().format_subscription_item_label(item_name_or_index) - if item_label and (display or self._display): - return f"{display or self._display}/{item_label}" - else: - return item_label - - def extract_subscription_item_name(self, item_label: str) -> str: - item_name = super().extract_subscription_item_name(item_label) - if self._display and item_name.startswith(self._display + "/"): - item_name = item_name[len(self._display) + 1 :] - return item_name - - def _get_undo_command_text( - self, index: QtCore.QModelIndex, column_desc: ColumnDesc - ) -> str: - text = super()._get_undo_command_text(index, column_desc) - if 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)})", - ) - return text - - 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) - or self._view_type_icons[item.type] - ) - else: - return None - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - if column_desc == self.NAME: - return self._get_subscription_color(item, column_desc) - else: - return None - - def _get_placeholder_view(self) -> View: - """ - Get a placeholder view to keep a display alive while modifying - (removing and restoring, in order) its actual views. - - :return: View instance - """ - color_space = ConfigCache.get_default_color_space_name() - if not color_space: - color_spaces = ConfigCache.get_color_space_names() - if color_spaces: - color_space = color_spaces[0] - else: - # Add a color space so a view can exist - color_space = "Raw" - config = ocio.GetCurrentConfig() - config.addColorSpace( - ocio.ColorSpace( - ocio.REFERENCE_SPACE_SCENE, - color_space, - bitDepth=ocio.BIT_DEPTH_F32, - isData=True, - ) - ) - - return View(ViewType.VIEW_SCENE, "_", color_space) - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[View]: - if not self._display: - return [] - - if ConfigCache.validate() and self._items: - return self._items - - config = ocio.GetCurrentConfig() - - self._items = [] - - # Display views - for name in config.getViews(ocio.VIEW_DISPLAY_DEFINED, self._display): - 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) - - else: # VIEW_DISPLAY - view = View( - view_type, - name, - config.getDisplayViewColorSpaceName(self._display, name), - config.getDisplayViewTransformName(self._display, name), - config.getDisplayViewLooks(self._display, name), - 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): - 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 - - def _clear_items(self) -> None: - if self._display: - config = ocio.GetCurrentConfig() - - # Insert placeholder view to keep display alive - placeholder_view = self._get_placeholder_view() - config.addDisplayView( - self._display, - placeholder_view.name, - placeholder_view.color_space, - ) - - # Views must be removed in reverse to preserve internal indices - for view in reversed(ConfigCache.get_views(self._display)): - if view != placeholder_view.name: - config.removeDisplayView(self._display, view) - - def _add_item(self, item: View) -> None: - if self._display: - config = ocio.GetCurrentConfig() - try: - if item.type == ViewType.VIEW_SHARED: - config.addDisplaySharedView(self._display, item.name) - elif item.type == ViewType.VIEW_DISPLAY: - config.addDisplayView( - self._display, - item.name, - item.view_transform, - item.color_space, - item.looks, - item.rule, - item.description, - ) - else: # ViewType.VIEW_SCENE - config.addDisplayView( - self._display, item.name, item.color_space, item.looks - ) - except ocio.Exception as e: - self.warning_raised.emit(str(e)) - - # Remove placeholder view, if present - placeholder_view = self._get_placeholder_view() - if placeholder_view.name in ConfigCache.get_views( - self._display, view_type=ocio.VIEW_DISPLAY_DEFINED - ): - config.removeDisplayView(self._display, placeholder_view.name) - - def _remove_item(self, item: View) -> None: - if self._display: - config = ocio.GetCurrentConfig() - config.removeDisplayView(self._display, item.name) - - def _new_item(self, name: str) -> None: - # New items added through presets only - pass - - def _get_value(self, item: View, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.VIEW_TYPE: - return item.type - elif column_desc == self.NAME: - return item.name - elif column_desc == self.COLOR_SPACE: - return item.color_space - elif column_desc == self.VIEW_TRANSFORM: - return item.view_transform - elif column_desc == self.LOOKS: - return item.looks - elif column_desc == self.RULE: - return item.rule - elif column_desc == self.DESCRIPTION: - return item.description - - # Invalid column - return None - - def _set_value( - 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: - return - - config = ocio.GetCurrentConfig() - prev_item_name = item.name - items = self._get_items() - item_index = item_names.index(item.name) - - self._clear_items() - - # Update parameters - if column_desc == self.COLOR_SPACE: - color_space = config.getColorSpace(value) - if color_space: - if ( - 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 - ): - items[item_index].view_transform = value - - elif column_desc == self.NAME: - items[item_index].name = value - elif column_desc == self.LOOKS: - items[item_index].looks = value - elif column_desc == self.RULE: - items[item_index].rule = value - elif column_desc == self.DESCRIPTION: - items[item_index].description = value - - for other_item in items: - self._add_item(other_item) - - # Make sure local item instance matches item in items list - item = items[item_index] - - if item.name != prev_item_name: - # Tell views to follow selection to new item - self.item_added.emit(item.name) - - self.item_renamed.emit(item.name, prev_item_name) - - # Broadcast transform or name changes to subscribers - if column_desc in ( - self.NAME, - self.COLOR_SPACE, - self.VIEW_TRANSFORM, - self.LOOKS, - ): - self._update_tf_subscribers( - item.name, - prev_item_name if prev_item_name != item.name else None, - ) diff --git a/src/apps/ocioview/ocioview/items/view_transform_edit.py b/src/apps/ocioview/ocioview/items/view_transform_edit.py deleted file mode 100644 index b200faf600..0000000000 --- a/src/apps/ocioview/ocioview/items/view_transform_edit.py +++ /dev/null @@ -1,126 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -from PySide6 import QtWidgets -import PyOpenColorIO as ocio - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from ..widgets import ( - EnumComboBox, - CallbackComboBox, - StringListWidget, - TextEdit, -) -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit -from .view_transform_model import ViewTransformModel - - -class ViewTransformParamEdit(BaseConfigItemParamEdit): - """ - Widget for editing the parameters and transforms for one view - transform. - """ - - __model_type__ = ViewTransformModel - __has_transforms__ = True - __from_ref_column_desc__ = ViewTransformModel.FROM_REFERENCE - __to_ref_column_desc__ = ViewTransformModel.TO_REFERENCE - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.reference_space_type_combo = EnumComboBox( - ocio.ReferenceSpaceType, - icons={ - ocio.REFERENCE_SPACE_SCENE: get_glyph_icon( - "ph.sun", size=ICON_SIZE_ITEM - ), - ocio.REFERENCE_SPACE_DISPLAY: get_glyph_icon( - "ph.monitor", size=ICON_SIZE_ITEM - ), - }, - ) - self.family_edit = CallbackComboBox( - ConfigCache.get_families, editable=True - ) - self.description_edit = TextEdit() - self.categories_list = StringListWidget( - item_basename="category", - item_icon=get_glyph_icon( - "ph.bookmarks-simple", 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._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 - ) - - def _get_available_categories(self) -> list[str]: - """ - :return: All unused categories which can be added as presets - to this item. - """ - current_categories = self.categories_list.items() - return [ - c - for c in ConfigCache.get_categories() - if c not in current_categories - ] - - -class ViewTransformEdit(BaseConfigItemEdit): - """ - Widget for editing all view transforms in the current config. - """ - - __param_edit_type__ = ViewTransformParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Map widgets to model columns - 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.param_edit.description_edit, model.DESCRIPTION.column - ) - 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 - ) - - # 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) - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) diff --git a/src/apps/ocioview/ocioview/items/view_transform_model.py b/src/apps/ocioview/ocioview/items/view_transform_model.py deleted file mode 100644 index 82593dc489..0000000000 --- a/src/apps/ocioview/ocioview/items/view_transform_model.py +++ /dev/null @@ -1,239 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import copy -from typing import Any, Optional, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..utils import get_enum_member, get_glyph_icon -from .config_item_model import ColumnDesc, BaseConfigItemModel -from .utils import ( - get_scene_to_display_transform, - get_display_to_scene_transform, -) - - -class ViewTransformModel(BaseConfigItemModel): - """ - Item model for editing view transforms in the current config. - """ - - REFERENCE_SPACE_TYPE = ColumnDesc(0, "Reference Space Type", int) - NAME = ColumnDesc(1, "Name", str) - FAMILY = ColumnDesc(2, "Family", str) - DESCRIPTION = ColumnDesc(3, "Description", str) - CATEGORIES = ColumnDesc(4, "Categories", list) - TO_REFERENCE = ColumnDesc(5, "To Reference", ocio.Transform) - FROM_REFERENCE = ColumnDesc(6, "From Reference", ocio.Transform) - - # fmt: off - COLUMNS = sorted([ - REFERENCE_SPACE_TYPE, NAME, FAMILY, DESCRIPTION, CATEGORIES, - TO_REFERENCE, FROM_REFERENCE, - ], key=lambda s: s.column) - # fmt: on - - __item_type__ = ocio.ViewTransform - __icon_glyph__ = "ph.intersect" - - 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_DISPLAY: get_glyph_icon( - "ph.monitor", size=ICON_SIZE_ITEM - ), - } - - def get_item_names(self) -> list[str]: - return [item.getName() for item in self._get_items()] - - def get_item_transforms( - self, item_label: str - ) -> tuple[Optional[ocio.Transform], Optional[ocio.Transform]]: - # Get view transform name from subscription item label - item_name = self.extract_subscription_item_name(item_label) - - config = ocio.GetCurrentConfig() - view_transform = config.getViewTransform(item_name) - if view_transform is not None: - return ( - get_scene_to_display_transform(view_transform), - get_display_to_scene_transform(view_transform), - ) - else: - return None, None - - def _get_icon( - self, item: ocio.ViewTransform, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - if column_desc == self.NAME: - return ( - self._get_subscription_icon(item, column_desc) - or self._ref_space_icons[item.getReferenceSpaceType()] - ) - else: - return None - - def _get_bg_color( - self, item: __item_type__, column_desc: ColumnDesc - ) -> Optional[QtGui.QColor]: - if column_desc == self.NAME: - return self._get_subscription_color(item, column_desc) - else: - return None - - def _get_items(self, preserve: bool = False) -> list[ocio.ViewTransform]: - if preserve: - self._items = [ - copy.deepcopy(item) - for item in ConfigCache.get_view_transforms() - ] - return self._items - else: - return ConfigCache.get_view_transforms() - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().clearViewTransforms() - - def _add_item(self, item: ocio.ViewTransform) -> None: - ocio.GetCurrentConfig().addViewTransform(item) - - def _remove_item(self, item: ocio.ViewTransform) -> None: - config = ocio.GetCurrentConfig() - items = [ - copy.deepcopy(other_item) - for other_item in config.getViewTransforms() - if other_item != item - ] - - config.clearViewTransforms() - - for other_item in items: - config.addViewTransform(other_item) - - def _new_item(self, name: str) -> None: - ocio.GetCurrentConfig().addViewTransform( - ocio.ViewTransform( - referenceSpace=ocio.REFERENCE_SPACE_SCENE, - name=name, - toReference=ocio.GroupTransform(), - fromReference=ocio.GroupTransform(), - ) - ) - - 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) - elif column_desc == self.NAME: - return item.getName() - elif column_desc == self.FAMILY: - return item.getFamily() - elif column_desc == self.DESCRIPTION: - return item.getDescription() - elif column_desc == self.CATEGORIES: - return list(item.getCategories()) - - # Get transforms - elif column_desc in (self.TO_REFERENCE, self.FROM_REFERENCE): - return item.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - if column_desc == self.TO_REFERENCE - else ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ) - - # Invalid column - return None - - def _set_value( - self, - item: ocio.ViewTransform, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - config = ocio.GetCurrentConfig() - new_item = None - prev_item_name = item.getName() - - # Changing reference space type requires constructing a new item - if column_desc == self.REFERENCE_SPACE_TYPE: - member = get_enum_member(ocio.ReferenceSpaceType, value) - if member is not None: - new_item = ocio.ViewTransform( - referenceSpace=member, - name=item.getName(), - family=item.getFamily(), - description=item.getDescription(), - toReference=item.getTransform( - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - ), - fromReference=item.getTransform( - ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE - ), - categories=list(item.getCategories()), - ) - - # Otherwise get an editable copy of the current item - if new_item is None: - new_item = copy.deepcopy(item) - - # Update parameters - if column_desc == self.NAME: - new_item.setName(value) - elif column_desc == self.FAMILY: - new_item.setFamily(value) - elif column_desc == self.DESCRIPTION: - new_item.setDescription(value) - elif column_desc == self.CATEGORIES: - new_item.clearCategories() - for category in value: - new_item.addCategory(category) - - # Update transforms - elif column_desc in (self.TO_REFERENCE, self.FROM_REFERENCE): - new_item.setTransform( - value, - ocio.VIEWTRANSFORM_DIR_TO_REFERENCE - if column_desc == self.TO_REFERENCE - else ocio.VIEWTRANSFORM_DIR_FROM_REFERENCE, - ) - - # Preserve item order when replacing item due to name or reference space - # 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() - ] - config.clearViewTransforms() - for other_item in items: - if other_item.getName() == prev_item_name: - config.addViewTransform(new_item) - item_name = new_item.getName() - if item_name != prev_item_name: - self.item_renamed.emit(item_name, prev_item_name) - else: - config.addViewTransform(other_item) - - # Item order is preserved for all other changes - else: - config.addViewTransform(new_item) - - # Broadcast transform or name changes to subscribers - 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, - ) diff --git a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py b/src/apps/ocioview/ocioview/items/viewing_rule_edit.py deleted file mode 100644 index fc72e28407..0000000000 --- a/src/apps/ocioview/ocioview/items/viewing_rule_edit.py +++ /dev/null @@ -1,186 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtWidgets - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..utils import get_glyph_icon -from ..widgets import ( - FormLayout, - LineEdit, - StringListWidget, - StringMapTableWidget, - ExpandingStackedWidget, -) -from .config_item_edit import BaseConfigItemParamEdit, BaseConfigItemEdit -from .delegates import ColorSpaceDelegate -from .viewing_rule_model import ViewingRuleType, ViewingRuleModel - - -class ViewingRuleParamEdit(BaseConfigItemParamEdit): - """Widget for editing the parameters for one viewing rule.""" - - __model_type__ = ViewingRuleModel - __has_transforms__ = False - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - # Widgets - self.name_edit_a = LineEdit() - self.color_space_list = StringListWidget( - item_basename="color space", - item_flags=QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable, - item_icon=ViewingRuleModel.get_rule_type_icon( - ViewingRuleType.RULE_COLOR_SPACE - ), - allow_empty=False, - get_presets=ConfigCache.get_color_space_names, - presets_only=True, - ) - self.color_space_list.view.setItemDelegate(ColorSpaceDelegate()) - self.custom_keys_table_a = StringMapTableWidget( - ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), - default_key_prefix="key_", - default_value="value", - ) - - self.name_edit_b = LineEdit() - self.encoding_list = StringListWidget( - item_basename="encoding", - item_flags=QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable, - item_icon=ViewingRuleModel.get_rule_type_icon( - ViewingRuleType.RULE_ENCODING - ), - allow_empty=False, - get_presets=ConfigCache.get_encodings, - presets_only=True, - ) - self.custom_keys_table_b = StringMapTableWidget( - ("Key Name", "Key Value"), - item_icon=get_glyph_icon("ph.key", size=ICON_SIZE_ITEM), - default_key_prefix="key_", - default_value="value", - ) - - # Layout - no_params = QtWidgets.QFrame() - - 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_a = QtWidgets.QFrame() - params_a.setLayout(params_layout_a) - - params_layout_b = FormLayout() - 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_b = QtWidgets.QFrame() - params_b.setLayout(params_layout_b) - - self._param_stack = ExpandingStackedWidget() - self._param_stack.addWidget(no_params) - self._param_stack.addWidget(params_a) - self._param_stack.addWidget(params_b) - - self._param_layout.removeRow(0) - self._param_layout.addRow(self._param_stack) - - self.model.item_removed.connect(self._on_item_removed) - - def reset(self) -> None: - super().reset() - self._param_stack.setCurrentIndex(0) - - def update_available_params( - 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 - ) - 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 - ) - self._param_stack.setCurrentIndex(2) - - def _on_item_removed(self) -> None: - """Hide rule widgets when no rule is present.""" - if not self.model.rowCount(): - self._param_stack.setCurrentIndex(0) - - -class ViewingRuleEdit(BaseConfigItemEdit): - """ - Widget for editing all viewing rules in the current config. - """ - - __param_edit_type__ = ViewingRuleParamEdit - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent=parent) - - model = self.model - - # Map widgets to model columns - self.mapper.addMapping( - self.param_edit.color_space_list, model.COLOR_SPACES.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 - ) - - # Initialize - if model.rowCount(): - self.list.set_current_row(0) - - @QtCore.Slot(int) - def _on_current_row_changed(self, row: int) -> None: - if row != -1: - # Update parameter widget states, since viewing rule type may differ from - # the previous rule. - viewing_rule_type = self.model.data( - self.model.index(row, self.model.VIEWING_RULE_TYPE.column), - QtCore.Qt.EditRole, - ) - self.param_edit.update_available_params( - self.mapper, viewing_rule_type - ) - - super()._on_current_row_changed(row) diff --git a/src/apps/ocioview/ocioview/items/viewing_rule_model.py b/src/apps/ocioview/ocioview/items/viewing_rule_model.py deleted file mode 100644 index 4f0c573b10..0000000000 --- a/src/apps/ocioview/ocioview/items/viewing_rule_model.py +++ /dev/null @@ -1,312 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import copy -import enum -from dataclasses import dataclass, field -from typing import Any, Optional, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from ..config_cache import ConfigCache -from ..constants import ICON_SIZE_ITEM -from ..undo import ConfigSnapshotUndoCommand -from ..utils import get_glyph_icon, next_name -from .config_item_model import ColumnDesc, BaseConfigItemModel - - -class ViewingRuleType(str, enum.Enum): - """Enum of viewing rule types.""" - - RULE_COLOR_SPACE = "Color Space Rule" - RULE_ENCODING = "Encoding Rule" - - -@dataclass -class ViewingRule: - """Individual viewing rule storage.""" - - type: ViewingRuleType - name: str - color_spaces: list[str] = field(default_factory=list) - encodings: list[str] = field(default_factory=list) - custom_keys: dict[str, str] = field(default_factory=dict) - - -class ViewingRuleModel(BaseConfigItemModel): - """ - Item model for editing viewing rules in the current config. - """ - - VIEWING_RULE_TYPE = ColumnDesc(0, "Viewing Rule Type", str) - NAME = ColumnDesc(1, "Name", str) - COLOR_SPACES = ColumnDesc(2, "Color Spaces", list) - ENCODINGS = ColumnDesc(3, "Encodings", list) - CUSTOM_KEYS = ColumnDesc(4, "Custom Keys", list) - - COLUMNS = sorted( - [VIEWING_RULE_TYPE, NAME, COLOR_SPACES, ENCODINGS, CUSTOM_KEYS], - key=lambda s: s.column, - ) - - __item_type__ = ViewingRule - __icon_glyph__ = "mdi6.eye-check-outline" - - @classmethod - def get_rule_type_icon(cls, rule_type: ViewingRuleType) -> QtGui.QIcon: - glyph_names = { - ViewingRuleType.RULE_COLOR_SPACE: "ph.swap", - ViewingRuleType.RULE_ENCODING: "mdi6.sine-wave", - } - return get_glyph_icon(glyph_names[rule_type], size=ICON_SIZE_ITEM) - - @classmethod - def has_presets(cls) -> bool: - return True - - @classmethod - def requires_presets(cls) -> bool: - return True - - @classmethod - def get_presets(cls) -> Optional[Union[list[str], dict[str, QtGui.QIcon]]]: - return { - ViewingRuleType.RULE_COLOR_SPACE.value: cls.get_rule_type_icon( - ViewingRuleType.RULE_COLOR_SPACE - ), - ViewingRuleType.RULE_ENCODING.value: cls.get_rule_type_icon( - ViewingRuleType.RULE_ENCODING - ), - } - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - self._rule_types = {} - - self._rule_type_icons = { - ViewingRuleType.RULE_COLOR_SPACE: self.get_rule_type_icon( - ViewingRuleType.RULE_COLOR_SPACE - ), - ViewingRuleType.RULE_ENCODING: self.get_rule_type_icon( - ViewingRuleType.RULE_ENCODING - ), - } - - ConfigCache.register_reset_callback(self._reset_cache) - - def add_preset(self, preset_name: str) -> int: - viewing_rules = self._get_editable_viewing_rules() - all_names = self.get_item_names() - item = None - - if preset_name == ViewingRuleType.RULE_COLOR_SPACE.value: - color_space = ConfigCache.get_default_color_space_name() - if not color_space: - color_spaces = ConfigCache.get_color_spaces() - if color_spaces: - color_space = color_spaces[0] - if color_space: - item = ViewingRule( - ViewingRuleType.RULE_COLOR_SPACE, - next_name("ColorSpaceRule_", all_names), - color_spaces=[color_space], - ) - else: - self.warning_raised.emit( - f"Could not create " - f"{ViewingRuleType.RULE_COLOR_SPACE.value.lower()} because no " - f"color spaces are defined." - ) - - else: # ViewingRuleType.RULE_ENCODING.value: - encodings = ConfigCache.get_encodings() - item = ViewingRule( - ViewingRuleType.RULE_ENCODING, - next_name("EncodingRule_", all_names), - encodings=[encodings[0]], - ) - - # Put new rule at top - row = -1 - if item is not None: - row = 0 - - with ConfigSnapshotUndoCommand( - f"Add {self.item_type_label()}", model=self, item_name=item.name - ): - self.beginInsertRows(self.NULL_INDEX, row, row) - self._insert_rule(row, viewing_rules, item) - - ocio.GetCurrentConfig().setViewingRules(viewing_rules) - - self.endInsertRows() - self.item_added.emit(item.name) - - return row - - def get_item_names(self) -> list[str]: - config = ocio.GetCurrentConfig() - viewing_rules = config.getViewingRules() - - return [viewing_rules.getName(i) for i in range(viewing_rules.getNumEntries())] - - def _get_icon( - self, item: ViewingRule, column_desc: ColumnDesc - ) -> Optional[QtGui.QIcon]: - if column_desc == self.NAME: - return self._rule_type_icons[item.type] - else: - return None - - def _reset_cache(self) -> None: - self._items = [] - - def _get_items(self, preserve: bool = False) -> list[ViewingRule]: - if ConfigCache.validate() and self._items: - return self._items - - config = ocio.GetCurrentConfig() - viewing_rules = config.getViewingRules() - self._items = [] - - for i in range(viewing_rules.getNumEntries()): - name = viewing_rules.getName(i) - color_spaces = list(viewing_rules.getColorSpaces(i)) - encodings = list(viewing_rules.getEncodings(i)) - - if color_spaces: - viewing_rule_type = ViewingRuleType.RULE_COLOR_SPACE - elif encodings: - viewing_rule_type = ViewingRuleType.RULE_ENCODING - else: - # Ambiguous rule type; drop it. - continue - - custom_keys = {} - for j in range(viewing_rules.getNumCustomKeys(i)): - key_name = viewing_rules.getCustomKeyName(i, j) - key_value = viewing_rules.getCustomKeyValue(i, j) - custom_keys[key_name] = key_value - - self._items.append( - ViewingRule( - viewing_rule_type, name, color_spaces, encodings, custom_keys - ) - ) - - return self._items - - def _clear_items(self) -> None: - ocio.GetCurrentConfig().setViewingRules(ocio.ViewingRules()) - - def _insert_rule( - self, index: int, viewing_rules: ocio.ViewingRules, item: ViewingRule - ) -> None: - """ - Insert rule into an ``ocio.ViewingRules`` object from a - ViewingRule instance. - """ - viewing_rules.insertRule(index, item.name) - - if item.type == ViewingRuleType.RULE_COLOR_SPACE: - for color_space in item.color_spaces: - viewing_rules.addColorSpace(index, color_space) - else: # ViewingRuleType.RULE_ENCODING - for encoding in item.encodings: - viewing_rules.addEncoding(index, encoding) - - for key_name, key_value in item.custom_keys.items(): - viewing_rules.setCustomKey(index, key_name, key_value) - - self._rule_types[item.name] = item.type - - def _remove_named_rule( - self, viewing_rules: ocio.ViewingRules, item: ViewingRule - ) -> None: - """Remove existing rule with name matching the provided rule.""" - for i in range(viewing_rules.getNumEntries()): - if viewing_rules.getName(i) == item.name: - viewing_rules.removeRule(i) - break - - self._rule_types.pop(item.name, None) - - def _get_editable_viewing_rules(self) -> ocio.ViewingRules: - """ - Copy existing config rules into new editable - ``ocio.ViewingRules`` instance. - """ - viewing_rules = ocio.ViewingRules() - for i, item in enumerate(self._get_items()): - self._insert_rule(i, viewing_rules, item) - return viewing_rules - - def _add_item(self, item: ViewingRule) -> None: - viewing_rules = self._get_editable_viewing_rules() - self._insert_rule(viewing_rules.getNumEntries(), viewing_rules, item) - ocio.GetCurrentConfig().setViewingRules(viewing_rules) - - def _new_item(self, name: str) -> None: - # Only presets can be added - pass - - def _remove_item(self, item: ViewingRule) -> None: - viewing_rules = self._get_editable_viewing_rules() - self._remove_named_rule(viewing_rules, item) - ocio.GetCurrentConfig().setViewingRules(viewing_rules) - - def _get_value(self, item: ViewingRule, column_desc: ColumnDesc) -> Any: - # Get parameters - if column_desc == self.VIEWING_RULE_TYPE: - return item.type - elif column_desc == self.NAME: - return item.name - elif column_desc == self.COLOR_SPACES: - return item.color_spaces - elif column_desc == self.ENCODINGS: - return item.encodings - elif column_desc == self.CUSTOM_KEYS: - return list(item.custom_keys.items()) - - # Invalid column - return None - - def _set_value( - self, - item: ViewingRule, - column_desc: ColumnDesc, - value: Any, - index: QtCore.QModelIndex, - ) -> None: - viewing_rules = self._get_editable_viewing_rules() - current_index = viewing_rules.getIndexForRule(item.name) - prev_item = copy.deepcopy(item) - - # Update parameters - if column_desc == self.NAME: - # Name must be unique - if value not in self.get_item_names(): - item.name = value - - elif column_desc == self.COLOR_SPACES: - if item.type == ViewingRuleType.RULE_COLOR_SPACE: - item.color_spaces = value - elif column_desc == self.ENCODINGS: - if item.type == ViewingRuleType.RULE_ENCODING: - item.encodings = value - - elif column_desc == self.CUSTOM_KEYS: - item.custom_keys.clear() - for key_name, key_value in value: - item.custom_keys[key_name] = key_value - - # Replace rule to update value - self._remove_named_rule(viewing_rules, prev_item) - self._insert_rule(current_index, viewing_rules, item) - - ocio.GetCurrentConfig().setViewingRules(viewing_rules) - - if item.name != prev_item.name: - self.item_renamed.emit(item.name, prev_item.name) diff --git a/src/apps/ocioview/ocioview/log_handlers.py b/src/apps/ocioview/ocioview/log_handlers.py deleted file mode 100644 index bf00b356e3..0000000000 --- a/src/apps/ocioview/ocioview/log_handlers.py +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import atexit -import logging -import sys -from logging.handlers import QueueHandler - -import PyOpenColorIO as ocio - -from .message_router import message_queue - - -# Queue handler -queue_handler = QueueHandler(message_queue) - -# Route OCIO log through queue handler, but disconnect for a clean exit -ocio.SetLoggingFunction(message_queue.put_nowait) -atexit.register(lambda: ocio.SetLoggingFunction(None)) - - -# stdout handler -class StdoutFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool: - if record.levelno != logging.ERROR: - return True - else: - return False - - -stdout_handler = logging.StreamHandler(sys.stdout) -stdout_handler.setLevel(logging.DEBUG) -stdout_handler.addFilter(StdoutFilter()) - - -# stderr handler -class StderrFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool: - if record.levelno == logging.ERROR: - return True - else: - return False - - -stderr_handler = logging.StreamHandler(sys.stderr) -stderr_handler.setLevel(logging.ERROR) -stderr_handler.addFilter(StderrFilter()) - - -# Configure application-wide logging -logging.root.name = "ocioview" -logging.addLevelName(logging.ERROR, "Error") -logging.addLevelName(logging.WARNING, "Warning") -logging.addLevelName(logging.INFO, "Info") -logging.addLevelName(logging.DEBUG, "Debug") - -logging.basicConfig( - level=logging.DEBUG, - handlers=[stdout_handler, stderr_handler, queue_handler], - format="[%(name)s %(levelname)s]: %(message)s", - force=True, -) - - -def set_logging_level(level: ocio.LoggingLevel) -> None: - """ - Change the OCIO and Python logging level. - - :param level: OCIO logging level - """ - ocio.SetLoggingLevel(level) - - if level == ocio.LOGGING_LEVEL_WARNING: - logging.root.setLevel(logging.WARNING) - elif level == ocio.LOGGING_LEVEL_INFO: - logging.root.setLevel(logging.INFO) - elif level == ocio.LOGGING_LEVEL_DEBUG: - logging.root.setLevel(logging.DEBUG) diff --git a/src/apps/ocioview/ocioview/main_window.py b/src/apps/ocioview/ocioview/main_window.py deleted file mode 100644 index c68d8d6bb6..0000000000 --- a/src/apps/ocioview/ocioview/main_window.py +++ /dev/null @@ -1,655 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import logging -import shutil -from pathlib import Path -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -from .config_cache import ConfigCache -from .config_dock import ConfigDock -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__) - - -class OCIOView(QtWidgets.QMainWindow): - """ - ocioview application main window. - """ - - # NOTE: Change this number when a major change to this widget's structure is - # implemented. This prevents conflicts when restoring QMainWindow state from - # settings. - SETTING_STATE_VERSION = 1 - - SETTING_GEOMETRY = "geometry" - SETTING_STATE = "state" - SETTING_CONFIG_DIR = "config_dir" - SETTING_RECENT_CONFIGS = "recent_configs" - SETTING_RECENT_CONFIG_PATH = "path" - - 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))) - - # Recent file menus - 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(corner_widget=self.mode_box) - - # Central widget - self.viewer_dock = ViewerDock(self.recent_images_menu) - - # Main menu - self.file_menu = QtWidgets.QMenu("File") - 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) - - 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...", - lambda: self.viewer_dock.load_image(), - QtGui.QKeySequence("Ctrl+I"), - ) - self.file_menu.addMenu(self.recent_images_menu) - self.file_menu.addAction( - "Load Image in New Tab...", - lambda: self.viewer_dock.load_image(new_tab=True), - QtGui.QKeySequence("Ctrl+Shift+I"), - ) - self.file_menu.addSeparator() - 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) - undo_action.setShortcut(QtGui.QKeySequence("Ctrl+Z")) - self.edit_menu.addAction(undo_action) - redo_action = undo_stack.createRedoAction(self.edit_menu) - redo_action.setShortcut(QtGui.QKeySequence("Ctrl+Shift+Z")) - self.edit_menu.addAction(redo_action) - self.edit_menu.addSeparator() - - self.menu_bar = QtWidgets.QMenuBar() - self.menu_bar.addMenu(self.file_menu) - self.menu_bar.addMenu(self.edit_menu) - self.setMenuBar(self.menu_bar) - - # Dock areas - self.setDockOptions( - 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 - ) - - for corner in (QtCore.Qt.TopLeftCorner, QtCore.Qt.BottomLeftCorner): - self.setCorner(corner, QtCore.Qt.LeftDockWidgetArea) - for corner in (QtCore.Qt.TopRightCorner, QtCore.Qt.BottomRightCorner): - self.setCorner(corner, QtCore.Qt.RightDockWidgetArea) - - # Layout - self.setCentralWidget(self.viewer_dock) - self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.inspect_dock) - self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.config_dock) - - # Connections - 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__) - if settings.contains(self.SETTING_GEOMETRY): - self.restoreGeometry(settings.value(self.SETTING_GEOMETRY)) - 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.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.new_config() - - self._update_recent_configs_menu() - self._update_window_title() - - # Start log processing - MessageRouter.get_instance().start_routing() - - def reset(self) -> None: - """ - Reset application from the current OCIO config. - """ - self._init_config_tracking() - - self.config_dock.reset() - self.inspect_dock.reset() - self.viewer_dock.reset() - - def closeEvent(self, event: QtGui.QCloseEvent) -> None: - if self._can_close_config(): - # Save settings - 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) - else: - event.ignore() - - def new_config(self) -> None: - """ - Create and load a new OCIO raw config. - """ - 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 - - config = ocio.Config.CreateRaw() - 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. - - :param config_path: Config file path - """ - if not self._can_close_config(): - return - - 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)", - ) - if not config_path_str: - return - - config_path = Path(config_path_str) - 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 - if not self._transient: - self._add_recent_config_path(self._config_path) - - # Reset application with empty config to clean all components - config = ocio.Config() - ocio.SetCurrentConfig(config) - self.reset() - - # Reset application again to update all components with the new config - config = ocio.Config.CreateFromFile(self._config_path.as_posix()) - 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 - path. If no config has been loaded, 'save_config_as' will be - called. - - :return: Whether config was saved - """ - if self._transient: - return False - elif self._config_path is None: - return self.save_config_as() - else: - try: - config_dir = self._config_path.parent - config_dir.mkdir(parents=True, exist_ok=True) - - config = ocio.GetCurrentConfig() - config.serialize(self._config_path.as_posix()) - - self._update_cache_id() - return True - - except Exception as e: - QtWidgets.QMessageBox.critical( - self, "Error", f"Config save failed with error: {str(e)}" - ) - logger.error(str(e), exc_info=e) - - return False - - def save_config_as(self, config_path: Optional[Path] = None) -> bool: - """ - Save the current OCIO config to a user specified path. - - :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( - self, - "Save Config", - dir=config_dir, - filter="OCIO Config (*.ocio)", - ) - if not config_path_str: - return False - - config_path = Path(config_path_str) - - self._config_path = config_path - - # Add path to recent config files - self._add_recent_config_path(self._config_path) - - config = ocio.GetCurrentConfig() - config.serialize(self._config_path.as_posix()) - - self._update_cache_id() - return True - - except Exception as e: - QtWidgets.QMessageBox.critical( - self, "Error", f"Config save failed with error: {str(e)}" - ) - logger.error(str(e), exc_info=e) - - return False - - def save_and_backup_config(self) -> bool: - """ - Save the config and make an incremental copy in a 'backup' - directory beside the config file. - - :return: Whether config was saved - """ - if self.save_config(): - try: - 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 - - except Exception as e: - QtWidgets.QMessageBox.critical( - self, "Error", f"Config backup failed with error: {str(e)}" - ) - logger.error(str(e), exc_info=e) - - return False - - def restore_config_backup(self) -> None: - """ - Browse for a config version from the 'backup' directory, and - restore it to memory after backing up the current config. - Calling save after restoring will save the restored config to - disk, making it the current config. - """ - if not self._can_close_config(): - return - - backup_dir = self._get_backup_dir() - if backup_dir is not None: - ( - version_path_str, - file_filter, - ) = QtWidgets.QFileDialog.getOpenFileName( - self, - "Restore Config", - dir=backup_dir.as_posix(), - filter="OCIO Config (*.ocio)", - ) - if not version_path_str: - return - - version_path = Path(version_path_str) - current_path = self._config_path - - # Backup current config to a new version and load the requested backup - # config in memory. - self.save_and_backup_config() - self.load_config(version_path) - - # Keep the internal config path set to the non-backup config path. If the - # user chooses to save, the loaded backup will become the current config - # version. - self._config_path = current_path - - def _get_next_version_path(self) -> Optional[Path]: - """ - Get the path to next backup version of the config. - - :return: Config version path - """ - backup_dir = self._get_backup_dir() - if backup_dir is None: - return None - - max_version = 0 - 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(): - other_version = int(other_version_str) - if other_version > max_version: - max_version = other_version - - return backup_dir / self._format_version_filename(max_version + 1) - - def _format_version_filename( - self, version_num: Optional[int] = None - ) -> Optional[str]: - """ - Format a config version filename, given a version number. - - :param version_num: Version number - :return: Config version filename - """ - if self._config_path is not None: - return ( - f"{self._config_path.stem}." - f"{'*' if not version_num else f'{version_num:04d}'}" - f"{self._config_path.suffix}" - ) - else: - return None - - def _get_backup_dir(self) -> Optional[Path]: - """ - :return: Config backup directory, which is created if it - doesn't exist yet. - """ - if self._config_path is not None and self._config_path.is_file(): - backup_dir = self._config_path.parent / "backup" - backup_dir.mkdir(parents=True, exist_ok=True) - return backup_dir - else: - return None - - def _get_config_dir(self, config_path: Optional[Path] = None) -> str: - """ - Infer a config save/load directory from an existing config path - or settings. - """ - config_dir = "" - if config_path is not None: - config_dir = config_path.parent.as_posix() - if not config_dir and self._config_path is not None: - config_dir = self._config_path.parent.as_posix() - if not config_dir and settings.contains(self.SETTING_CONFIG_DIR): - config_dir = settings.value(self.SETTING_CONFIG_DIR) - return config_dir - - def _get_recent_config_paths(self) -> list[Path]: - """ - Get the 10 most recently loaded or saved config file paths that - still exist. - - :return: List of OCIO config file paths - """ - recent_configs = [] - - 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 - ) - if recent_config_path_str: - recent_config_path = Path(recent_config_path_str) - if recent_config_path.is_file(): - recent_configs.append(recent_config_path) - settings.endArray() - - return recent_configs - - def _add_recent_config_path(self, config_path: Path) -> None: - """ - Add the provided config file path to the top of the recent - config files list. - - :param config_path: OCIO config file path - """ - config_paths = self._get_recent_config_paths() - if config_path in config_paths: - config_paths.remove(config_path) - config_paths.insert(0, config_path) - - if len(config_paths) > 10: - config_paths = config_path[:10] - - settings.beginWriteArray(self.SETTING_RECENT_CONFIGS) - for i, recent_config_path in enumerate(config_paths): - settings.setArrayIndex(i) - settings.setValue( - self.SETTING_RECENT_CONFIG_PATH, recent_config_path.as_posix() - ) - settings.endArray() - - # Update menu with latest list - self._update_recent_configs_menu() - - def _update_recent_configs_menu(self) -> None: - """Update recent configs menu actions.""" - self.recent_configs_menu.clear() - for recent_config_path in self._get_recent_config_paths(): - self.recent_configs_menu.addAction( - recent_config_path.name, - lambda path=recent_config_path: self.load_config(path), - ) - - def _update_window_title(self) -> None: - filename = ( - "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}") - - def _update_cache_id(self): - """ - Update cache ID which represents config state at the last save, - for determining whether unsaved changes exist. - """ - config_cache_id, is_valid = ConfigCache.get_cache_id() - if is_valid: - self._config_save_cache_id = config_cache_id - self._update_window_title() - - 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 - - def _can_close_config(self) -> bool: - """ - Ask user if changes should be saved. - - :return: True if changes were saved or discarded, in which case - the config can be closed, or False if the operation was - cancelled and the config should remain open for editing. - """ - if self._has_unsaved_changes(): - button = QtWidgets.QMessageBox.warning( - self, - "Save Changes?", - "The current config has been modified. Would you like to save your " - "changes before closing? All unsaved changes will be lost if " - "discarded.", - QtWidgets.QMessageBox.Save - | QtWidgets.QMessageBox.Discard - | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.Cancel, - ) - if button == QtWidgets.QMessageBox.Save: - # Save changes. Ok to close config if save is successful. - return self.save_config() - elif button == QtWidgets.QMessageBox.Discard: - # Changes discarded. Ok to close config. - return True - else: - # Changes not saved or discarded. Keep editing confing. - return False - - # No unsaved changes - return True - - def _init_config_tracking(self) -> None: - """Setup app-dependent config objects and change tracking.""" - ReferenceSpaceManager.init_reference_spaces() - self._update_cache_id() - - @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 deleted file mode 100644 index 402d940a9f..0000000000 --- a/src/apps/ocioview/ocioview/message_router.py +++ /dev/null @@ -1,428 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -import logging -import re -import sys -from typing import Any, Optional -from queue import Empty, SimpleQueue - -import numpy as np -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -from .processor_context import ProcessorContext -from .utils import ( - config_to_html, - processor_to_ctf_html, - processor_to_shader_html, -) - - -# Global message queue -message_queue = SimpleQueue() - - -class MessageRunner(QtCore.QObject): - """ - Object for routing OCIO and Python messages to listeners from a - background thread. - """ - - info_logged = QtCore.Signal(str) - warning_logged = QtCore.Signal(str) - error_logged = QtCore.Signal(str) - debug_logged = QtCore.Signal(str) - - config_html_ready = QtCore.Signal(str) - ctf_html_ready = QtCore.Signal(str, ocio.GroupTransform) - image_ready = QtCore.Signal(np.ndarray) - processor_ready = QtCore.Signal(ProcessorContext, ocio.CPUProcessor) - shader_html_ready = QtCore.Signal(str, ocio.GPUProcessor) - - LOOP_INTERVAL = 0.5 # In seconds - - FMT_LOG = f"{{html}}" # Just triggers Qt HTML detection - FMT_ERROR = ( - f'{{html}}' - ) - FMT_WARNING = ( - f'{{html}}' - ) - - RE_LOG_LEVEL = re.compile(r"\s*\[\w+ (?P[a-zA-Z]+)]:") - - LOG_LEVEL_ERROR = "error" - LOG_LEVEL_WARNING = "warning" - LOG_LEVEL_INFO = "info" - LOG_LEVEL_DEBUG = "debug" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - self._is_routing = False - self._end_routing = True - self._gpu_language = ocio.GPU_LANGUAGE_GLSL_4_0 - - self._prev_config = None - self._prev_proc_data = None - self._prev_image_array = None - - self._config_updates_allowed = False - self._ctf_updates_allowed = False - self._image_updates_allowed = False - self._processor_updates_allowed = False - self._shader_updates_allowed = False - - @property - def gpu_language(self) -> ocio.GpuLanguage: - return self._gpu_language - - @gpu_language.setter - def gpu_language(self, gpu_language: ocio.GpuLanguage) -> None: - self._gpu_language = gpu_language - if self._shader_updates_allowed and self._prev_proc_data is not None: - # Rebroadcast last processor record - message_queue.put_nowait(self._prev_proc_data) - - @property - def config_updates_allowed(self) -> bool: - return self._config_updates_allowed - - @config_updates_allowed.setter - def config_updates_allowed(self, allowed: bool) -> None: - self._config_updates_allowed = allowed - if allowed and self._prev_config is not None: - # Rebroadcast last config record - message_queue.put_nowait(self._prev_config) - - @property - def ctf_updates_allowed(self) -> bool: - return self._ctf_updates_allowed - - @ctf_updates_allowed.setter - def ctf_updates_allowed(self, allowed: bool) -> None: - self._ctf_updates_allowed = allowed - if allowed and self._prev_proc_data is not None: - # Rebroadcast last processor record - message_queue.put_nowait(self._prev_proc_data) - - @property - def image_updates_allowed(self) -> bool: - return self._image_updates_allowed - - @image_updates_allowed.setter - def image_updates_allowed(self, allowed: bool) -> None: - self._image_updates_allowed = allowed - if allowed and self._prev_image_array is not None: - # Rebroadcast last image record - message_queue.put_nowait(self._prev_image_array) - - @property - def processor_updates_allowed(self) -> bool: - return self._processor_updates_allowed - - @processor_updates_allowed.setter - def processor_updates_allowed(self, allowed: bool) -> None: - self._processor_updates_allowed = allowed - if allowed and self._prev_proc_data is not None: - # Rebroadcast last processor record - message_queue.put_nowait(self._prev_proc_data) - - @property - def shader_updates_allowed(self) -> bool: - return self._shader_updates_allowed - - @shader_updates_allowed.setter - def shader_updates_allowed(self, allowed: bool) -> None: - self._shader_updates_allowed = allowed - if allowed and self._prev_proc_data is not None: - # Rebroadcast last processor record - message_queue.put_nowait(self._prev_proc_data) - - def is_routing(self) -> bool: - """Whether runner is routing messages.""" - return self._is_routing - - def end_routing(self) -> None: - """Instruct runner to exit routing loop ASAP.""" - self._end_routing = True - - def start_routing(self) -> None: - """Instruct runner to start routing messages.""" - self._end_routing = False - - while not self._end_routing: - self._is_routing = True - - try: - msg_raw = message_queue.get(timeout=self.LOOP_INTERVAL) - except Empty: - continue - - # OCIO config - if isinstance(msg_raw, ocio.Config): - self._prev_config = msg_raw - if self._config_updates_allowed: - self._handle_config_message(msg_raw) - - # OCIO processor - elif ( - isinstance(msg_raw, tuple) - and len(msg_raw) == 2 - and isinstance(msg_raw[0], (str, ProcessorContext)) - and isinstance(msg_raw[1], ocio.Processor) - ): - self._prev_proc_data = msg_raw - if ( - self._processor_updates_allowed - or self._ctf_updates_allowed - or self._shader_updates_allowed - ): - self._handle_processor_message(*msg_raw) - - # Image array - elif isinstance(msg_raw, np.ndarray): - self._prev_image_array = msg_raw - if self._image_updates_allowed: - self._handle_image_message(msg_raw) - - # Python or OCIO log record - else: - self._handle_log_message(msg_raw) - - self._is_routing = False - - def _handle_config_message(self, config: ocio.Config) -> None: - """ - Handle OCIO config received in the message queue. - - :param config: OCIO config instance - """ - try: - config_html_data = config_to_html(config) - 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 - ) - - def _handle_processor_message( - self, - proc_context: ProcessorContext, - proc: ocio.Processor, - ) -> None: - """ - Handle OCIO processor received in the message queue. - - :param proc_context: OCIO processor context data - :param proc: OCIO processor instance - """ - try: - if self._processor_updates_allowed: - self.processor_ready.emit( - proc_context, proc.getDefaultCPUProcessor() - ) - - if self._ctf_updates_allowed: - ctf_html_data, group_tf = processor_to_ctf_html(proc) - self.ctf_html_ready.emit(ctf_html_data, group_tf) - - if self._shader_updates_allowed: - gpu_proc = proc.getDefaultGPUProcessor() - shader_html_data = processor_to_shader_html( - gpu_proc, self._gpu_language - ) - self.shader_html_ready.emit(shader_html_data, gpu_proc) - - except Exception as e: - # Pass error to log - self._handle_log_message( - str(e), force_level=self.LOG_LEVEL_WARNING - ) - - def _handle_image_message(self, image_array: np.ndarray) -> None: - """ - Handle image buffer received in the message queue. - - :param image_array: Image array - """ - try: - 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 - ) - - def _handle_log_message( - self, log_record: str, force_level: Optional[str] = None - ) -> None: - """ - Handle routing a Python or OCIO log record received in the - message queue. - - :param log_record: Log record data - :param force_level: Force a particular log level for this - record. - """ - # Python log record - if isinstance(log_record, logging.LogRecord): - level = log_record.levelname.lower() - msg = log_record.msg - - # OCIO log record? - else: - level = self.LOG_LEVEL_INFO - msg = str(log_record) - - record_match = self.RE_LOG_LEVEL.match(log_record) - if record_match: - level = record_match.group("level").lower() - - # Route non-debug OCIO messages to stdout/stderr also - if level == self.LOG_LEVEL_ERROR: - sys.stderr.write(msg) - elif level in (self.LOG_LEVEL_WARNING, self.LOG_LEVEL_INFO): - sys.stdout.write(msg) - - # Override inferred level? - if force_level is not None: - level = force_level - - # HTML conversion - html_msg = msg.rstrip().replace(" ", " ").replace("\n", "
") - - # Python and OCIO log output - if level == self.LOG_LEVEL_ERROR: - self.error_logged.emit(self.FMT_ERROR.format(html=html_msg)) - elif level == self.LOG_LEVEL_WARNING: - self.warning_logged.emit(self.FMT_WARNING.format(html=html_msg)) - elif level == self.LOG_LEVEL_INFO: - self.info_logged.emit(self.FMT_LOG.format(html=html_msg)) - elif level == self.LOG_LEVEL_DEBUG: - self.debug_logged.emit(self.FMT_LOG.format(html=html_msg)) - - -class MessageRouter(QtCore.QObject): - """ - Singleton router which runs a background thread for routing a - variety of messages and log records to listeners. - """ - - __instance: MessageRouter = None - - @classmethod - def get_instance(cls) -> MessageRouter: - """Get singleton MessageRouter instance.""" - if cls.__instance is None: - cls.__instance = MessageRouter() - 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 - - # Setup threading - self._thread = QtCore.QThread() - self._runner = MessageRunner() - self._runner.moveToThread(self._thread) - - # 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() - app.aboutToQuit.connect(self.end_routing) - - def __getattr__(self, item: str) -> Any: - """Forward unknown attribute requests to internal runner.""" - return getattr(self._runner, item) - - @property - def gpu_language(self) -> ocio.GpuLanguage: - return self._runner.gpu_language - - @gpu_language.setter - def gpu_language(self, gpu_language: ocio.GpuLanguage) -> None: - self._runner.gpu_language = gpu_language - - @property - def config_updates_allowed(self) -> bool: - return self._runner.config_updates_allowed - - @config_updates_allowed.setter - def config_updates_allowed(self, allowed: bool) -> None: - self._runner.config_updates_allowed = allowed - - @property - def ctf_updates_allowed(self) -> bool: - return self._runner.ctf_updates_allowed - - @ctf_updates_allowed.setter - def ctf_updates_allowed(self, allowed: bool) -> None: - self._runner.ctf_updates_allowed = allowed - - @property - def image_updates_allowed(self) -> bool: - return self._runner.image_updates_allowed - - @image_updates_allowed.setter - def image_updates_allowed(self, allowed: bool) -> None: - self._runner.image_updates_allowed = allowed - - @property - def processor_updates_allowed(self) -> bool: - return self._runner.processor_updates_allowed - - @processor_updates_allowed.setter - def processor_updates_allowed(self, allowed: bool) -> None: - self._runner.processor_updates_allowed = allowed - - @property - def shader_updates_allowed(self) -> bool: - return self._runner.shader_updates_allowed - - @shader_updates_allowed.setter - def shader_updates_allowed(self, allowed: bool) -> None: - self._runner.shader_updates_allowed = allowed - - def end_routing(self) -> None: - """Stop message routing thread.""" - if not self._runner.is_routing(): - return - - self._runner.end_routing() - self._thread.quit() - - # Wait twice as long as the routing loop interval for thread to stop. If - # quitting the thread takes longer than this, the app will exit with a non-zero - # exit code. - self._thread.wait(int(MessageRunner.LOOP_INTERVAL * 1000)) - - def start_routing(self) -> None: - """Start message routing thread.""" - if self._runner.is_routing(): - return - - self._thread.start() diff --git a/src/apps/ocioview/ocioview/mode.py b/src/apps/ocioview/ocioview/mode.py deleted file mode 100644 index 4bf08bb11f..0000000000 --- a/src/apps/ocioview/ocioview/mode.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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/processor_context.py b/src/apps/ocioview/ocioview/processor_context.py deleted file mode 100644 index 832d63858d..0000000000 --- a/src/apps/ocioview/ocioview/processor_context.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Type - -import PyOpenColorIO as ocio - - -@dataclass -class ProcessorContext: - """ - Data about current config items that constructed a processor. - """ - - input_color_space: str | None = field(default_factory=lambda: None) - """Input color space name.""" - - transform_item_type: Type | None = field(default_factory=lambda: None) - """Transform source config item type.""" - - transform_item_name: str | None = field(default_factory=lambda: None) - """Transform source config item name.""" - - transform_direction: ocio.TransformDirection = ocio.TRANSFORM_DIR_FORWARD - """Transform direction being viewed.""" diff --git a/src/apps/ocioview/ocioview/ref_space_manager.py b/src/apps/ocioview/ocioview/ref_space_manager.py deleted file mode 100644 index 022dc927df..0000000000 --- a/src/apps/ocioview/ocioview/ref_space_manager.py +++ /dev/null @@ -1,177 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Callable, Optional - -import PyOpenColorIO as ocio - - -class ReferenceSpaceManager: - """Interface for managing config reference spaces.""" - - _ref_scene_name: Optional[str] = None - _ref_display_name: Optional[str] = None - _ref_subscribers: list[Callable] = [] - - @classmethod - def init_reference_spaces(cls) -> None: - cls._update_scene_reference_space() - cls._update_display_reference_space() - - @classmethod - def subscribe_to_reference_spaces(cls, ref_callback: Callable) -> None: - """ - Subscribe to reference space updates. - - :param ref_callback: Reference space callback, which will be - called when any reference space is created, notifying the - application of an external config change. - """ - if ref_callback not in cls._ref_subscribers: - cls._ref_subscribers.append(ref_callback) - - @classmethod - def scene_reference_space(cls) -> ocio.ColorSpace: - """ - Return a color space from the current config which is - representative of its scene reference space. If no such color - space exists, one will be created. - - :return: Scene reference color space - """ - cls._update_scene_reference_space() - - config = ocio.GetCurrentConfig() - return config.getColorSpace(cls._ref_scene_name) - - @classmethod - def display_reference_space(cls) -> ocio.ColorSpace: - """ - Return a color space from the current config which is - representative of its display reference space. If no such color - space exists, one will be created. - - :return: Display reference color space - """ - cls._update_display_reference_space() - - config = ocio.GetCurrentConfig() - return config.getColorSpace(cls._ref_display_name) - - @classmethod - def _update_scene_reference_space(cls) -> None: - """ - Find or create a color space which is representative of the - current config's scene reference space. This color space will - have no transforms and not be a data space. - """ - config = ocio.GetCurrentConfig() - - # Verify existing scene reference space - if cls._ref_scene_name: - scene_ref_color_space = config.getColorSpace(cls._ref_scene_name) - if ( - not scene_ref_color_space - or scene_ref_color_space.getReferenceSpaceType() - != ocio.REFERENCE_SPACE_SCENE - or scene_ref_color_space.isData() - or scene_ref_color_space.getTransform( - ocio.COLORSPACE_DIR_FROM_REFERENCE - ) - or scene_ref_color_space.getTransform( - ocio.COLORSPACE_DIR_TO_REFERENCE - ) - ): - cls._ref_scene_name = None - - if not cls._ref_scene_name: - # Find first candidate scene reference space - for color_space in config.getColorSpaces( - ocio.SEARCH_REFERENCE_SPACE_SCENE, ocio.COLORSPACE_ALL - ): - 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 - ) - ): - cls._ref_scene_name = color_space.getName() - break - - # Make a scene reference space if not found - if not cls._ref_scene_name: - scene_ref_color_space = ocio.ColorSpace( - ocio.REFERENCE_SPACE_SCENE, - "scene_reference", - bitDepth=ocio.BIT_DEPTH_F32, - isData=False, - ) - config.addColorSpace(scene_ref_color_space) - cls._ref_scene_name = scene_ref_color_space.getName() - - # Notify subscribers - for callback in cls._ref_subscribers: - callback() - - @classmethod - def _update_display_reference_space(cls) -> None: - """ - Find or create a color space which is representative of the - current config's display reference space. This display color - space will have no transforms and not be a data space. - """ - config = ocio.GetCurrentConfig() - - # Verify existing display reference space - if 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() - != ocio.REFERENCE_SPACE_DISPLAY - or display_ref_color_space.isData() - or display_ref_color_space.getTransform( - ocio.COLORSPACE_DIR_FROM_REFERENCE - ) - or display_ref_color_space.getTransform( - ocio.COLORSPACE_DIR_TO_REFERENCE - ) - ): - cls._ref_display_name = None - - if not cls._ref_display_name: - # Find first candidate display reference space - for color_space in config.getColorSpaces( - ocio.SEARCH_REFERENCE_SPACE_DISPLAY, ocio.COLORSPACE_ALL - ): - 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 - ) - ): - cls._ref_display_name = color_space.getName() - break - - # Make a display reference space if not found - if not cls._ref_display_name: - display_ref_color_space = ocio.ColorSpace( - ocio.REFERENCE_SPACE_DISPLAY, - "display_reference", - bitDepth=ocio.BIT_DEPTH_F32, - isData=False, - ) - config.addColorSpace(display_ref_color_space) - cls._ref_display_name = display_ref_color_space.getName() - - # Notify subscribers - for callback in cls._ref_subscribers: - callback() diff --git a/src/apps/ocioview/ocioview/settings.py b/src/apps/ocioview/ocioview/settings.py deleted file mode 100644 index 9f2c5ac7b8..0000000000 --- a/src/apps/ocioview/ocioview/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from PySide6 import QtCore - - -settings = QtCore.QSettings( - QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, "OpenColorIO", "ocioview" -) -"""Global application settings.""" diff --git a/src/apps/ocioview/ocioview/setup.py b/src/apps/ocioview/ocioview/setup.py deleted file mode 100644 index 33699bad6f..0000000000 --- a/src/apps/ocioview/ocioview/setup.py +++ /dev/null @@ -1,81 +0,0 @@ -# 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 deleted file mode 100644 index 7ed7834a17..0000000000 --- a/src/apps/ocioview/ocioview/signal_router.py +++ /dev/null @@ -1,85 +0,0 @@ -# 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/style.py b/src/apps/ocioview/ocioview/style.py deleted file mode 100644 index 1f35c1564d..0000000000 --- a/src/apps/ocioview/ocioview/style.py +++ /dev/null @@ -1,117 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtGui, QtWidgets - -from .constants import ( - BORDER_COLOR_ROLE, - TOOL_BAR_BORDER_COLOR_ROLE, - TOOL_BAR_BG_COLOR_ROLE, -) - - -# Application style sheet overrides -QSS = """ -""" - - -class DarkPalette(QtGui.QPalette): - def __init__(self): - super().__init__() - - # Central roles - self.setColor(QtGui.QPalette.Window, QtGui.QColor(60, 60, 60)) - self.setColor(QtGui.QPalette.WindowText, QtGui.QColor(190, 190, 190)) - self.setColor(QtGui.QPalette.Base, QtGui.QColor(50, 50, 50)) - self.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(45, 45, 45)) - self.setColor(QtGui.QPalette.ToolTipBase, QtGui.QColor(45, 45, 45)) - self.setColor(QtGui.QPalette.ToolTipText, QtGui.QColor(190, 190, 190)) - self.setColor(QtGui.QPalette.PlaceholderText, QtGui.QColor(100, 100, 100)) - self.setColor(QtGui.QPalette.Text, QtGui.QColor(190, 190, 190)) - self.setColor(QtGui.QPalette.Button, QtGui.QColor(50, 50, 50)) - self.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(190, 190, 190)) - self.setColor(QtGui.QPalette.BrightText, QtGui.QColor(190, 190, 190)) - - # 3D effects - self.setColor(QtGui.QPalette.Dark, QtGui.QColor(47, 47, 47)) - self.setColor(QtGui.QPalette.Mid, QtGui.QColor(64, 64, 64)) - self.setColor(QtGui.QPalette.Midlight, QtGui.QColor(81, 81, 81)) - - # Highlight - self.setColor(QtGui.QPalette.Highlight, QtGui.QColor(204, 30, 104)) - self.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(190, 190, 190)) - - # Disabled - disabled = QtGui.QPalette.Disabled - self.setColor(disabled, QtGui.QPalette.Base, QtGui.QColor(57, 57, 57)) - self.setColor(disabled, QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) - self.setColor(disabled, QtGui.QPalette.Button, QtGui.QColor(57, 57, 57)) - self.setColor(disabled, QtGui.QPalette.ButtonText, QtGui.QColor(115, 115, 115)) - - -def apply_top_tool_bar_style( - widget: QtWidgets.QWidget, - bg_color_role: Optional[QtGui.QPalette.ColorRole] = TOOL_BAR_BG_COLOR_ROLE, - border_color_role: QtGui.QPalette.ColorRole = TOOL_BAR_BORDER_COLOR_ROLE, - border_bottom_radius: int = 0, -): - """ - Applies a style to the provided widget which wraps it in a styled - box with rounded top corners, intended as a toolbar visually - attached to the widget below it. - - .. note:: - The supplied widget MUST have a unique object name for the - style to apply correctly. - - :param widget: Widget to style - :param bg_color_role: Optional BG QPalette color role. If not - specified the current BG color is preserved. - :param border_color_role: Border QPalette color role - :param border_bottom_radius: Corner radius for bottom of toolbar - """ - palette = widget.palette() - border_color = palette.color(border_color_role).name() - - qss = f"QFrame#{widget.objectName()} {{" - if bg_color_role is not None: - qss += f" background-color: {palette.color(bg_color_role).name()};" - qss += ( - f" border-top: 1px solid {border_color};" - f" border-right: 1px solid {border_color};" - f" border-left: 1px solid {border_color};" - f" border-top-left-radius: 3px;" - f" border-top-right-radius: 3px;" - f" border-bottom-left-radius: {border_bottom_radius:d}px;" - f" border-bottom-right-radius: {border_bottom_radius:d}px;" - f"}}" - ) - widget.setStyleSheet(qss) - - -def apply_widget_with_top_tool_bar_style( - widget: QtWidgets.QWidget, - border_color_role: QtGui.QPalette.ColorRole = BORDER_COLOR_ROLE, -): - """ - Applies a style to the provided widget which wraps it in a styled - box with rounded corners, intended to visually tie together a - widget with its styled top toolbar. - - .. note:: - The supplied widget MUST have a unique object name for the - style to apply correctly. - - :param widget: Widget to style - :param border_color_role: Border QPalette color role - """ - palette = widget.palette() - - widget.setStyleSheet( - f"QFrame#base_log_view__log_inner_frame {{" - f" border: 1px solid {palette.color(border_color_role).name()};" - f" border-radius: 3px;" - f"}}" - ) diff --git a/src/apps/ocioview/ocioview/transform_manager.py b/src/apps/ocioview/ocioview/transform_manager.py deleted file mode 100644 index 45950b048c..0000000000 --- a/src/apps/ocioview/ocioview/transform_manager.py +++ /dev/null @@ -1,341 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass -from functools import partial -from typing import Callable, Optional, Type, Union - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from .constants import ICON_SIZE_ITEM -from .utils import get_glyph_icon - - -@dataclass -class TransformSubscription: - """Reference for one item transform subscription.""" - - item_model: QtCore.QAbstractItemModel - item_label: str - - -class TransformAgent(QtCore.QObject): - """ - Agent which manages transform fulfillment for a subscription slot. - """ - - item_name_changed = QtCore.Signal(str) - item_tf_changed = QtCore.Signal(ocio.Transform, ocio.Transform) - - def __init__(self, slot: int, parent: Optional[QtCore.QObject] = None): - """ - :param slot: Subscription slot number (0-9) - """ - super().__init__(parent=parent) - - self._slot = slot - - @property - def slot(self) -> int: - """ - :return: Subscription slot number - """ - return self._slot - - def disconnect_all(self) -> None: - """Disconnect all signals.""" - for signal in (self.item_name_changed, self.item_tf_changed): - try: - signal.disconnect() - except RuntimeError: - # Signal already disconnected - pass - - -class TransformManager: - """Interface for managing transform subscriptions and subscribers.""" - - _tf_subscriptions: dict[int, TransformSubscription] = {} - _tf_subscribers: dict[int, list[Callable]] = defaultdict(list) - _tf_menu_subscribers: list[Callable] = [] - - @classmethod - def set_subscription( - cls, slot: int, item_model: QtCore.QAbstractItemModel, item_label: str - ) -> None: - """ - Set the transform for a specific subscription slot, so that - item transform and name changes are broadcast to all slot - subscribers. - - :param slot: Subscription slot number between 1-10 - :param item_model: Model for item and its transforms - :param item_label: Item label - """ - prev_item_model = None - - # Disconnect previous subscription on target slot - if slot in cls._tf_subscriptions: - tf_subscription = cls._tf_subscriptions.pop(slot) - prev_item_model = tf_subscription.item_model - tf_agent = tf_subscription.item_model.get_transform_agent(slot) - tf_agent.disconnect_all() - - # Disconnect other slots with the same item reference - for other_slot, tf_subscription in list(cls._tf_subscriptions.items()): - if ( - tf_subscription.item_model == item_model - and tf_subscription.item_label == item_label - ): - tf_agent = tf_subscription.item_model.get_transform_agent(slot) - tf_agent.disconnect_all() - del cls._tf_subscriptions[other_slot] - - # Connect new subscription - tf_subscription = TransformSubscription(item_model, item_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) - ) - cls._tf_subscriptions[slot] = tf_subscription - - # Inform menu subscribers of the menu change - cls._update_menu_items() - - # Inform init subscribers of the new subscription - for init_callback in cls._tf_subscribers.get(-1, []): - init_callback(slot) - - # Trigger immediate update to subscribers of this slot - 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: - prev_item_model.repaint() - if prev_item_model is None or prev_item_model != item_model: - item_model.repaint() - - @classmethod - def get_subscription_slot( - cls, item_model: QtCore.QAbstractItemModel, item_label: str - ) -> int: - """ - Return the subscription slot number for a transform - with the provided item model and label, if set. - - :param item_model: Model for item and its transforms - :param item_label: Item label - :return: Subscription slot number, or -1 if no subscription is - set. - """ - for slot, tf_subscription in cls._tf_subscriptions.items(): - if ( - tf_subscription.item_model == item_model - and tf_subscription.item_label == item_label - ): - return slot - return -1 - - @classmethod - def get_subscription_slot_color( - cls, slot: int, saturation: float = 0.5, value: float = 1.0 - ) -> Union[QtGui.QColor, None]: - """ - Return a standard subscription slot color for use in GUI - elements. - - :param slot: Subscription slot number - :param saturation: Adjust the color's saturation, which - defaults to 0.5. - :param value: Adjust the color's value, which defaults to 1.0 - :return: QColor, if slot number is valid - """ - if slot != -1: - return QtGui.QColor.fromHsvF(slot / 10.0, saturation, value) - else: - return None - - @classmethod - def get_subscription_slot_icon(cls, slot: int) -> Union[QtGui.QIcon, None]: - """ - Return a standard subscription slot icon for use in GUI - elements. - - :param slot: Subscription slot number - :return: Colorized QIcon if slot number is valid - """ - if slot != -1: - slot_word = { - 0: "zero", - 1: "one", - 2: "two", - 3: "three", - 4: "four", - 5: "five", - 6: "six", - 7: "seven", - 8: "eight", - 9: "nine", - }[slot] - color = cls.get_subscription_slot_color(slot) - return get_glyph_icon( - f"ph.number-circle-{slot_word}", - color=color, - size=ICON_SIZE_ITEM, - ) - else: - return None - - @classmethod - def get_subscription_menu_items( - cls, - ) -> list[tuple[int, str, Type, str, QtGui.QIcon]]: - """ - :return: Subscription slots, their labels, associated item - types and names, and slot icons, for use in subscription - menus. - """ - return [ - ( - i, - s.item_label, - s.item_model.__item_type__, - s.item_model.extract_subscription_item_name(s.item_label), - cls.get_subscription_slot_icon(i), - ) - for i, s in sorted(cls._tf_subscriptions.items()) - ] - - @classmethod - def subscribe_to_transform_menu(cls, menu_callback: Callable) -> None: - """ - Subscribe to transform menu updates, to be notified when any - subscription changes. - - :param menu_callback: Menu callback, which will be called with - a list of tuples with subscription slot, transform name, - and item icon for each subscription whenever one changes. - """ - cls._tf_menu_subscribers.append(menu_callback) - - # Trigger immediate menu update to new subscriber - menu_callback(cls.get_subscription_menu_items()) - - @classmethod - def subscribe_to_transform_subscription_init( - cls, init_callback: Callable - ) -> None: - """ - Subscribe to transform subscription initialization on all slots. - - :param init_callback: Transform subscription initialization - callback, which will be called whenever a new transform - subscription is initialized with the subscription slot - number. - """ - cls._tf_subscribers[-1].append(init_callback) - - # Trigger immediate update to init subscriber if a transform subscription - # exists. - for slot, tf_subscription in cls._tf_subscriptions.items(): - init_callback(slot) - break - - @classmethod - def subscribe_to_transforms_at( - cls, slot: int, tf_callback: Callable - ) -> None: - """ - Subscribe to transform updates at the given slot number. - - :param slot: Subscription slot number - :param tf_callback: Transform callback, which will be called - with the subscription slot number, and forward and inverse - transforms when either change. All transforms assume a - scene reference space input. - """ - tf_subscription = cls._tf_subscriptions.get(slot) - cls._tf_subscribers[slot].append(tf_callback) - - # Trigger immediate update to new subscriber - if tf_subscription is not None: - tf_callback( - slot, - *tf_subscription.item_model.get_item_transforms( - tf_subscription.item_label - ), - ) - - @classmethod - def unsubscribe_from_all_transforms(cls, tf_callback: Callable) -> None: - """ - Unsubscribe from transform and item name updates at all slot - numbers. - - :param tf_callback: Previously subscribed transform callback - """ - for slot, callbacks in cls._tf_subscribers.items(): - if tf_callback in callbacks: - callbacks.remove(tf_callback) - - @classmethod - def reset(cls) -> None: - """ - Drop all transform subscriptions and broadcast empty menus to - subscribers. - """ - for slot in reversed(list(cls._tf_subscriptions.keys())): - tf_subscription = cls._tf_subscriptions.pop(slot) - tf_agent = tf_subscription.item_model.get_transform_agent(slot) - tf_agent.disconnect_all() - - # Trigger immediate update to subscribers - cls._update_menu_items() - - @classmethod - def _update_menu_items(cls) -> None: - """Tell all menu subscribers to update their item names.""" - menu_items = cls.get_subscription_menu_items() - for callback in cls._tf_menu_subscribers: - callback(menu_items) - - @classmethod - def _on_item_name_changed(cls, slot: int, item_label: str) -> None: - """ - Called when a subscription item is renamed, for internal and - subscriber tracking. - """ - tf_subscription = cls._tf_subscriptions.get(slot) - if tf_subscription is not None: - tf_subscription.item_label = item_label - cls._on_item_tf_changed( - slot, - *tf_subscription.item_model.get_item_transforms(item_label), - ) - cls._update_menu_items() - - @classmethod - def _on_item_tf_changed( - cls, - slot: int, - item_tf_fwd: Optional[ocio.Transform], - item_tf_inv: Optional[ocio.Transform], - ) -> None: - """ - Called when a subscription transform is updated, for internal - and subscriber tracking. - """ - tf_subscription = cls._tf_subscriptions.get(slot) - tf_subscribers = cls._tf_subscribers.get(slot) - if tf_subscription is not None and tf_subscribers: - for callback in tf_subscribers: - callback(slot, item_tf_fwd, item_tf_inv) diff --git a/src/apps/ocioview/ocioview/transforms/__init__.py b/src/apps/ocioview/ocioview/transforms/__init__.py deleted file mode 100644 index 854fe42cea..0000000000 --- a/src/apps/ocioview/ocioview/transforms/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from .allocation_edit import AllocationTransformEdit -from .builtin_edit import BuiltinTransformEdit -from .cdl_edit import CDLTransformEdit -from .color_space_edit import ColorSpaceTransformEdit -from .display_view_edit import DisplayViewTransformEdit -from .exponent_edit import ExponentTransformEdit -from .exponent_with_linear_edit import ExponentWithLinearTransformEdit -from .exposure_contrast_edit import ExposureContrastTransformEdit -from .file_edit import FileTransformEdit -from .fixed_function_edit import FixedFunctionTransformEdit -from .log_edit import LogTransformEdit -from .log_affine_edit import LogAffineTransformEdit -from .log_camera_edit import LogCameraTransformEdit -from .look_edit import LookTransformEdit -from .matrix_edit import MatrixTransformEdit -from .range_edit import RangeTransformEdit - -from .transform_edit_factory import TransformEditFactory -from .transform_edit_stack import TransformEditStack diff --git a/src/apps/ocioview/ocioview/transforms/allocation_edit.py b/src/apps/ocioview/ocioview/transforms/allocation_edit.py deleted file mode 100644 index 2e7bde4a0f..0000000000 --- a/src/apps/ocioview/ocioview/transforms/allocation_edit.py +++ /dev/null @@ -1,69 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import EnumComboBox, FloatEdit, FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class AllocationTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.memory" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widgets - self.allocation_combo = EnumComboBox(ocio.Allocation) - self.allocation_combo.currentIndexChanged.connect(self._on_allocation_changed) - self.allocation_combo.currentIndexChanged.connect(self._on_edit) - - self.src_range_edit = FloatEditArray(("min", "max"), (0.0, 1.0)) - self.src_range_edit.value_changed.connect(self._on_edit) - - self.lin_offset_edit = FloatEdit(0.0) - self.lin_offset_edit.value_changed.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Linear Offset", self.lin_offset_edit) - self.tf_layout.insertRow(0, "Source Range", self.src_range_edit) - self.tf_layout.insertRow(0, "Allocation", self.allocation_combo) - - # Initialize - self._on_allocation_changed(self.allocation_combo.currentIndex()) - - def transform(self) -> ocio.ColorSpaceTransform: - allocation = self.allocation_combo.member() - vars_ = self.src_range_edit.value() - if allocation == ocio.ALLOCATION_LG2: - vars_.append(self.lin_offset_edit.value()) - - transform = super().transform() - transform.setAllocation(allocation) - transform.setVars(vars_) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.allocation_combo.set_member(transform.getAllocation()) - alloc_vars = transform.getVars() or [0.0, 1.0] - if len(alloc_vars) >= 2: - self.src_range_edit.set_value(alloc_vars[:2]) - if len(alloc_vars) > 2: - self.lin_offset_edit.set_value(alloc_vars[2]) - - @QtCore.Slot(int) - def _on_allocation_changed(self, index: int): - """ - Toggle enabled variable widgets for the selected allocation. - """ - allocation = self.allocation_combo.member() - self.src_range_edit.setEnabled(allocation != ocio.ALLOCATION_UNKNOWN) - self.lin_offset_edit.setEnabled(allocation == ocio.ALLOCATION_LG2) - - -TransformEditFactory.register(ocio.AllocationTransform, AllocationTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/builtin_edit.py b/src/apps/ocioview/ocioview/transforms/builtin_edit.py deleted file mode 100644 index 2be865f246..0000000000 --- a/src/apps/ocioview/ocioview/transforms/builtin_edit.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import ComboBox -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class BuiltinTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.package" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.style_combo = ComboBox() - - registry = ocio.BuiltinTransformRegistry() - tooltip_lines = [] - for style, desc in registry.getBuiltins(): - self.style_combo.addItem(style) - tooltip_lines.append(f"{style}: {desc}") - - self.style_combo.setCurrentText(transform.getStyle()) - self.style_combo.setToolTip("\n".join(tooltip_lines)) - self.style_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Style", self.style_combo) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setStyle(self.style_combo.currentText()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.style_combo.setCurrentText(transform.getStyle()) - - -TransformEditFactory.register(ocio.BuiltinTransform, BuiltinTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/cdl_edit.py b/src/apps/ocioview/ocioview/transforms/cdl_edit.py deleted file mode 100644 index 5f404f187b..0000000000 --- a/src/apps/ocioview/ocioview/transforms/cdl_edit.py +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..constants import RGB -from ..widgets import EnumComboBox, FloatEdit, FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class CDLTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.circles-three" - __tf_type_label__ = "CDL" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.slope_edit = FloatEditArray(RGB, transform.getSlope()) - self.slope_edit.value_changed.connect(self._on_edit) - - self.offset_edit = FloatEditArray(RGB, transform.getOffset()) - self.offset_edit.value_changed.connect(self._on_edit) - - self.power_edit = FloatEditArray(RGB, transform.getPower()) - self.power_edit.value_changed.connect(self._on_edit) - - self.sat_edit = FloatEdit(transform.getSat()) - self.sat_edit.value_changed.connect(self._on_edit) - - self.style_combo = EnumComboBox(ocio.CDLStyle) - self.style_combo.set_member(transform.getStyle()) - self.style_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Style", self.style_combo) - self.tf_layout.insertRow(0, "Saturation", self.sat_edit) - self.tf_layout.insertRow(0, "Power", self.power_edit) - self.tf_layout.insertRow(0, "Offset", self.offset_edit) - self.tf_layout.insertRow(0, "Slope", self.slope_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setSlope(self.slope_edit.value()) - transform.setOffset(self.offset_edit.value()) - transform.setPower(self.power_edit.value()) - transform.setSat(self.sat_edit.value()) - transform.setStyle(self.style_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.slope_edit.set_value(transform.getSlope()) - self.offset_edit.set_value(transform.getOffset()) - self.power_edit.set_value(transform.getPower()) - self.sat_edit.set_value(transform.getSat()) - self.style_combo.set_member(transform.getStyle()) - - -TransformEditFactory.register(ocio.CDLTransform, CDLTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/color_space_edit.py b/src/apps/ocioview/ocioview/transforms/color_space_edit.py deleted file mode 100644 index e432f85865..0000000000 --- a/src/apps/ocioview/ocioview/transforms/color_space_edit.py +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import CheckBox, ColorSpaceComboBox -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class ColorSpaceTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.swap" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widget - self.src_combo = ColorSpaceComboBox(include_roles=True) - self.src_combo.color_space_changed.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) - - # Layout - self.tf_layout.insertRow(0, "", self.data_bypass_check) - self.tf_layout.insertRow(0, "Destination", self.dst_combo) - self.tf_layout.insertRow(0, "Source", self.src_combo) - - # Initialize - self.update_from_config() - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - 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.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_color_spaces() - self.dst_combo.update_color_spaces() - - -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 deleted file mode 100644 index 1894ffed2b..0000000000 --- a/src/apps/ocioview/ocioview/transforms/display_view_edit.py +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..config_cache import ConfigCache -from ..utils import SignalsBlocked -from ..widgets import CheckBox, ComboBox, CallbackComboBox, ColorSpaceComboBox -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class DisplayViewTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.monitor-eye" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widget - 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_edit) - - self.view_combo = ComboBox() - self.view_combo.currentIndexChanged.connect(self._on_edit) - - self.looks_bypass_check = CheckBox("Looks Bypass") - self.looks_bypass_check.stateChanged.connect(self._on_edit) - - self.data_bypass_check = CheckBox("Data Bypass") - self.data_bypass_check.stateChanged.connect(self._on_edit) - - # Layout - bypass_layout = QtWidgets.QHBoxLayout() - bypass_layout.addWidget(self.looks_bypass_check) - bypass_layout.addWidget(self.data_bypass_check) - bypass_layout.addStretch() - - self.tf_layout.insertRow(0, "", bypass_layout) - self.tf_layout.insertRow(0, "View", self.view_combo) - self.tf_layout.insertRow(0, "Display", self.display_combo) - self.tf_layout.insertRow(0, "Source", self.src_combo) - - # Initialize - self.update_from_config() - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - 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()) - 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.set_color_space(transform.getSrc()) - self.display_combo.setCurrentText(transform.getDisplay()) - self.view_combo.setCurrentText(transform.getView()) - self.looks_bypass_check.setChecked(transform.getLooksBypass()) - self.data_bypass_check.setChecked(transform.getDataBypass()) - - def update_from_config(self): - """ - Update available color spaces and displays from the current - config. - """ - self.src_combo.update_color_spaces() - self.display_combo.update_items() - self._on_display_changed(self.display_combo.currentIndex()) - - @QtCore.Slot(int) - def _on_display_changed(self, index: int): - """ - Update available views for the selected display from the - current config. - """ - config = ocio.GetCurrentConfig() - display = self.display_combo.itemText(index) - view = self.view_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) - ) - - view_index = self.view_combo.findText(view) - if view_index != -1: - self.view_combo.setCurrentIndex(view_index) - else: - self.view_combo.setCurrentText( - config.getDefaultView(display, color_space_name) - ) - - -TransformEditFactory.register( - ocio.DisplayViewTransform, DisplayViewTransformEdit -) diff --git a/src/apps/ocioview/ocioview/transforms/exponent_edit.py b/src/apps/ocioview/ocioview/transforms/exponent_edit.py deleted file mode 100644 index c5ed3dba20..0000000000 --- a/src/apps/ocioview/ocioview/transforms/exponent_edit.py +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..constants import RGBA -from ..widgets import EnumComboBox, FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class ExponentTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.exponent" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.value_edit = FloatEditArray(RGBA, transform.getValue()) - self.value_edit.value_changed.connect(self._on_edit) - - self.negative_style_combo = EnumComboBox(ocio.NegativeStyle) - self.negative_style_combo.set_member(transform.getNegativeStyle()) - self.negative_style_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Negative Style", self.negative_style_combo) - self.tf_layout.insertRow(0, "Value", self.value_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setValue(self.value_edit.value()) - transform.setNegativeStyle(self.negative_style_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.value_edit.set_value(transform.getValue()) - self.negative_style_combo.set_member(transform.getNegativeStyle()) - - -TransformEditFactory.register(ocio.ExponentTransform, ExponentTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/exponent_with_linear_edit.py b/src/apps/ocioview/ocioview/transforms/exponent_with_linear_edit.py deleted file mode 100644 index 004bdb02fc..0000000000 --- a/src/apps/ocioview/ocioview/transforms/exponent_with_linear_edit.py +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..constants import RGBA -from ..widgets import EnumComboBox, FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class ExponentWithLinearTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.exponent-box" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.gamma_edit = FloatEditArray(RGBA, transform.getGamma()) - self.gamma_edit.value_changed.connect(self._on_edit) - - self.offset_edit = FloatEditArray(RGBA, transform.getOffset()) - self.offset_edit.value_changed.connect(self._on_edit) - - self.negative_style_combo = EnumComboBox(ocio.NegativeStyle) - self.negative_style_combo.set_member(transform.getNegativeStyle()) - self.negative_style_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Negative Style", self.negative_style_combo) - self.tf_layout.insertRow(0, "Offset", self.offset_edit) - self.tf_layout.insertRow(0, "Gamma", self.gamma_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setGamma(self.gamma_edit.value()) - transform.setOffset(self.offset_edit.value()) - transform.setNegativeStyle(self.negative_style_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.gamma_edit.set_value(transform.getGamma()) - self.offset_edit.set_value(transform.getOffset()) - self.negative_style_combo.set_member(transform.getNegativeStyle()) - - -TransformEditFactory.register( - ocio.ExponentWithLinearTransform, ExponentWithLinearTransformEdit -) diff --git a/src/apps/ocioview/ocioview/transforms/exposure_contrast_edit.py b/src/apps/ocioview/ocioview/transforms/exposure_contrast_edit.py deleted file mode 100644 index 891eade24d..0000000000 --- a/src/apps/ocioview/ocioview/transforms/exposure_contrast_edit.py +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import EnumComboBox, FloatEdit -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class ExposureContrastTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.sliders-horizontal" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.style_combo = EnumComboBox(ocio.ExposureContrastStyle) - self.style_combo.set_member(transform.getStyle()) - self.style_combo.currentIndexChanged.connect(self._on_edit) - - self.exposure_edit = FloatEdit(transform.getExposure()) - self.exposure_edit.value_changed.connect(self._on_edit) - - self.contrast_edit = FloatEdit(transform.getContrast()) - self.contrast_edit.value_changed.connect(self._on_edit) - - self.gamma_edit = FloatEdit(transform.getGamma()) - self.gamma_edit.value_changed.connect(self._on_edit) - - self.pivot_edit = FloatEdit(transform.getPivot()) - self.pivot_edit.value_changed.connect(self._on_edit) - - self.log_exposure_step_edit = FloatEdit(transform.getLogExposureStep()) - self.log_exposure_step_edit.value_changed.connect(self._on_edit) - - self.log_mid_gray_edit = FloatEdit(transform.getLogMidGray()) - self.log_mid_gray_edit.value_changed.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Log Mid Gray", self.log_mid_gray_edit) - self.tf_layout.insertRow(0, "Log Exposure Step", self.log_exposure_step_edit) - self.tf_layout.insertRow(0, "Pivot", self.pivot_edit) - self.tf_layout.insertRow(0, "Gamma", self.gamma_edit) - self.tf_layout.insertRow(0, "Contrast", self.contrast_edit) - self.tf_layout.insertRow(0, "Exposure", self.exposure_edit) - self.tf_layout.insertRow(0, "Style", self.style_combo) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setStyle(self.style_combo.member()) - transform.setExposure(self.exposure_edit.value()) - transform.setContrast(self.contrast_edit.value()) - transform.setGamma(self.gamma_edit.value()) - transform.setPivot(self.pivot_edit.value()) - transform.setLogExposureStep(self.log_exposure_step_edit.value()) - transform.setLogMidGray(self.log_mid_gray_edit.value()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.style_combo.set_member(transform.getStyle()) - self.exposure_edit.set_value(transform.getExposure()) - self.contrast_edit.set_value(transform.getContrast()) - self.gamma_edit.set_value(transform.getGamma()) - self.pivot_edit.set_value(transform.getPivot()) - self.log_exposure_step_edit.set_value(transform.getLogExposureStep()) - self.log_mid_gray_edit.set_value(transform.getLogMidGray()) - - -TransformEditFactory.register( - ocio.ExposureContrastTransform, ExposureContrastTransformEdit -) diff --git a/src/apps/ocioview/ocioview/transforms/file_edit.py b/src/apps/ocioview/ocioview/transforms/file_edit.py deleted file mode 100644 index 464eb0ca90..0000000000 --- a/src/apps/ocioview/ocioview/transforms/file_edit.py +++ /dev/null @@ -1,72 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import os -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import EnumComboBox, LineEdit -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class FileTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.file-table-outline" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.src_edit = LineEdit() - self.src_edit.editingFinished.connect(self._on_src_changed) - self.src_edit.editingFinished.connect(self._on_edit) - - self.ccc_id_edit = LineEdit() - self.ccc_id_edit.setEnabled(False) - self.ccc_id_edit.editingFinished.connect(self._on_edit) - - self.cdl_style_combo = EnumComboBox(ocio.CDLStyle) - self.cdl_style_combo.set_member(transform.getCDLStyle()) - self.cdl_style_combo.setEnabled(False) - self.cdl_style_combo.currentIndexChanged.connect(self._on_edit) - - self.interpolation_combo = EnumComboBox(ocio.Interpolation) - self.interpolation_combo.set_member(transform.getInterpolation()) - self.interpolation_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Interpolation", self.interpolation_combo) - self.tf_layout.insertRow(0, "CDL Style", self.cdl_style_combo) - self.tf_layout.insertRow(0, "CCC ID", self.ccc_id_edit) - self.tf_layout.insertRow(0, "Source", self.src_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setSrc(self.src_edit.text()) - transform.setCCCId(self.ccc_id_edit.text()) - transform.setInterpolation(self.interpolation_combo.member()) - transform.setCDLStyle(self.cdl_style_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.src_edit.setText(transform.getSrc()) - self.ccc_id_edit.setText(transform.getCCCId()) - self.interpolation_combo.set_member(transform.getInterpolation()) - self.cdl_style_combo.set_member(transform.getCDLStyle()) - - def _on_src_changed(self): - """ - Toggle file format specific widgets based on the file - extension. - """ - filename, ext = os.path.splitext(self.src_edit.text()) - self.ccc_id_edit.setEnabled(ext == ".ccc") - self.cdl_style_combo.setEnabled(ext in (".cdl", ".cc", ".ccc")) - - -TransformEditFactory.register(ocio.FileTransform, FileTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/fixed_function_edit.py b/src/apps/ocioview/ocioview/transforms/fixed_function_edit.py deleted file mode 100644 index ebf7019087..0000000000 --- a/src/apps/ocioview/ocioview/transforms/fixed_function_edit.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..widgets import EnumComboBox, FloatEditArray, ExpandingStackedWidget -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class FixedFunctionTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.function-variant" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widgets - self.style_combo = EnumComboBox(ocio.FixedFunctionStyle) - self.style_combo.currentIndexChanged.connect(self._on_style_changed) - self.style_combo.currentIndexChanged.connect(self._on_edit) - - self.no_params = QtWidgets.QLabel("no params") - self.no_params.setDisabled(True) - - self.rec2100_surround_edit = FloatEditArray(("gamma",)) - self.rec2100_surround_edit.value_changed.connect(self._on_edit) - - self.aces_gamut_comp_13_edit = FloatEditArray( - ("lim c", "lim m", "lim y", "thr c", "thr m", "thr y", "power"), - shape=(3, 3), - ) - self.aces_gamut_comp_13_edit.value_changed.connect(self._on_edit) - - self.param_widgets = { - ocio.FIXED_FUNCTION_REC2100_SURROUND: self.rec2100_surround_edit, - ocio.FIXED_FUNCTION_ACES_GAMUT_COMP_13: self.aces_gamut_comp_13_edit, - } - - self.param_stack = ExpandingStackedWidget() - self.param_stack.addWidget(self.no_params) - self.param_stack.addWidget(self.rec2100_surround_edit) - self.param_stack.addWidget(self.aces_gamut_comp_13_edit) - - # Layout - self.tf_layout.insertRow(0, "Params", self.param_stack) - self.tf_layout.insertRow(0, "Style", self.style_combo) - - # Initialize - self._on_style_changed(self.style_combo.currentIndex()) - - def transform(self) -> ocio.ColorSpaceTransform: - style = self.style_combo.member() - param_widget = self.param_widgets.get(style) - params = [] - if param_widget is not None: - params = param_widget.value() - - transform = self.__tf_type__(style) - transform.setParams(params) - transform.setDirection(self.direction_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - - style = transform.getStyle() - param_widget = self.param_widgets.get(style) - if param_widget is not None: - param_widget.set_value(transform.getParams()) - - self.style_combo.set_member(style) - - @QtCore.Slot(int) - def _on_style_changed(self, index: int): - """ - Toggle style-specific parameter widgets in the parameter stack. - """ - style = self.style_combo.member() - widget = self.param_widgets.get(style, self.no_params) - self.param_stack.setCurrentWidget(widget) - - -TransformEditFactory.register(ocio.FixedFunctionTransform, FixedFunctionTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/log_affine_edit.py b/src/apps/ocioview/ocioview/transforms/log_affine_edit.py deleted file mode 100644 index 4190c96912..0000000000 --- a/src/apps/ocioview/ocioview/transforms/log_affine_edit.py +++ /dev/null @@ -1,62 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..constants import RGB -from ..widgets import FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class LogAffineTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.chart-bell-curve-cumulative" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.log_side_slope_edit = FloatEditArray(RGB, transform.getLogSideSlopeValue()) - self.log_side_slope_edit.value_changed.connect(self._on_edit) - - self.log_side_offset_edit = FloatEditArray( - RGB, transform.getLogSideOffsetValue() - ) - self.log_side_offset_edit.value_changed.connect(self._on_edit) - - self.lin_side_slope_edit = FloatEditArray(RGB, transform.getLinSideSlopeValue()) - self.lin_side_slope_edit.value_changed.connect(self._on_edit) - - self.lin_side_offset_edit = FloatEditArray( - RGB, transform.getLinSideOffsetValue() - ) - self.lin_side_offset_edit.value_changed.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Lin Side Offset", self.lin_side_offset_edit) - self.tf_layout.insertRow(0, "Lin Side Slope", self.lin_side_slope_edit) - self.tf_layout.insertRow(0, "Log Side Offset", self.log_side_offset_edit) - self.tf_layout.insertRow(0, "Log Side Slope", self.log_side_slope_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setLogSideSlopeValue(self.log_side_slope_edit.value()) - transform.setLogSideOffsetValue(self.log_side_offset_edit.value()) - transform.setLinSideSlopeValue(self.lin_side_slope_edit.value()) - transform.setLinSideOffsetValue(self.lin_side_offset_edit.value()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.log_side_slope_edit.set_value(transform.getLogSideSlopeValue()) - self.log_side_offset_edit.set_value(transform.getLogSideOffsetValue()) - self.lin_side_slope_edit.set_value(transform.getLinSideSlopeValue()) - self.lin_side_offset_edit.set_value(transform.getLinSideOffsetValue()) - - -TransformEditFactory.register(ocio.LogAffineTransform, LogAffineTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/log_camera_edit.py b/src/apps/ocioview/ocioview/transforms/log_camera_edit.py deleted file mode 100644 index ddc8650006..0000000000 --- a/src/apps/ocioview/ocioview/transforms/log_camera_edit.py +++ /dev/null @@ -1,82 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..constants import RGB -from ..widgets import FloatEdit, FloatEditArray -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class LogCameraTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.video-camera" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.__tf_type__([0.1, 0.1, 0.1]) - - # Widgets - self.base_edit = FloatEdit(transform.getBase()) - self.base_edit.value_changed.connect(self._on_edit) - - self.log_side_slope_edit = FloatEditArray(RGB, transform.getLogSideSlopeValue()) - self.log_side_slope_edit.value_changed.connect(self._on_edit) - - self.log_side_offset_edit = FloatEditArray( - RGB, transform.getLogSideOffsetValue() - ) - self.log_side_offset_edit.value_changed.connect(self._on_edit) - - self.lin_side_slope_edit = FloatEditArray(RGB, transform.getLinSideSlopeValue()) - self.lin_side_slope_edit.value_changed.connect(self._on_edit) - - self.lin_side_offset_edit = FloatEditArray( - RGB, transform.getLinSideOffsetValue() - ) - self.lin_side_offset_edit.value_changed.connect(self._on_edit) - - self.lin_side_break_edit = FloatEditArray(RGB, transform.getLinSideBreakValue()) - self.lin_side_break_edit.value_changed.connect(self._on_edit) - - # Unset linear slope is NaN - self.linear_slope_edit = FloatEditArray(RGB, (0.0, 0.0, 0.0)) - self.linear_slope_edit.value_changed.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Linear Slope", self.linear_slope_edit) - self.tf_layout.insertRow(0, "Lin Side Break", self.lin_side_break_edit) - self.tf_layout.insertRow(0, "Lin Side Offset", self.lin_side_offset_edit) - self.tf_layout.insertRow(0, "Lin Side Slope", self.lin_side_slope_edit) - self.tf_layout.insertRow(0, "Log Side Offset", self.log_side_offset_edit) - self.tf_layout.insertRow(0, "Log Side Slope", self.log_side_slope_edit) - self.tf_layout.insertRow(0, "Base", self.base_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = self.__tf_type__(self.lin_side_break_edit.value()) - transform.setBase(self.base_edit.value()) - transform.setLogSideSlopeValue(self.log_side_slope_edit.value()) - transform.setLogSideOffsetValue(self.log_side_offset_edit.value()) - transform.setLinSideSlopeValue(self.lin_side_slope_edit.value()) - transform.setLinSideOffsetValue(self.lin_side_offset_edit.value()) - transform.setLinSideBreakValue(self.lin_side_break_edit.value()) - transform.setLinearSlopeValue(self.linear_slope_edit.value()) - transform.setDirection(self.direction_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.base_edit.set_value(transform.getBase()) - self.log_side_slope_edit.set_value(transform.getLogSideSlopeValue()) - self.log_side_offset_edit.set_value(transform.getLogSideOffsetValue()) - self.lin_side_slope_edit.set_value(transform.getLinSideSlopeValue()) - self.lin_side_offset_edit.set_value(transform.getLinSideOffsetValue()) - self.lin_side_break_edit.set_value(transform.getLinSideBreakValue()) - self.linear_slope_edit.set_value(transform.getLinearSlopeValue()) - - -TransformEditFactory.register(ocio.LogCameraTransform, LogCameraTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/log_edit.py b/src/apps/ocioview/ocioview/transforms/log_edit.py deleted file mode 100644 index 638eb94170..0000000000 --- a/src/apps/ocioview/ocioview/transforms/log_edit.py +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import FloatEdit -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class LogTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.math-log" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - transform = self.create_transform() - - # Widgets - self.base_edit = FloatEdit(transform.getBase()) - self.base_edit.value_changed.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "Base", self.base_edit) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - transform.setBase(self.base_edit.value()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.base_edit.set_value(transform.getBase()) - - -TransformEditFactory.register(ocio.LogTransform, LogTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/look_edit.py b/src/apps/ocioview/ocioview/transforms/look_edit.py deleted file mode 100644 index 086a203083..0000000000 --- a/src/apps/ocioview/ocioview/transforms/look_edit.py +++ /dev/null @@ -1,74 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore - -from ..widgets import CheckBox, ColorSpaceComboBox, LineEdit -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class LookTransformEdit(BaseTransformEdit): - __icon_glyph__ = "ph.aperture" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widgets - self.src_combo = ColorSpaceComboBox(include_roles=True) - self.src_combo.color_space_changed.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 - ) - - # TODO: Add look completer and validator - self.looks_edit = LineEdit() - self.looks_edit.editingFinished.connect(self._on_edit) - - # Layout - self.tf_layout.insertRow(0, "", self.skip_color_space_conversion_check) - self.tf_layout.insertRow(0, "Looks", self.looks_edit) - self.tf_layout.insertRow(0, "Destination", self.dst_combo) - self.tf_layout.insertRow(0, "Source", self.src_combo) - - # initialize - self.update_from_config() - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - 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() - ) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - 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() - ) - - def update_from_config(self): - """ - Update available color spaces from current config. - """ - 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/matrix_edit.py b/src/apps/ocioview/ocioview/transforms/matrix_edit.py deleted file mode 100644 index 5cd48068b0..0000000000 --- a/src/apps/ocioview/ocioview/transforms/matrix_edit.py +++ /dev/null @@ -1,144 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import RGB, RGBA -from ..widgets import ( - ComboBox, - FloatEdit, - FloatEditArray, - IntEditArray, - FormLayout, - ExpandingStackedWidget, -) -from ..utils import m44_to_m33, m33_to_m44, v4_to_v3, v3_to_v4 -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class MatrixTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.matrix" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - config = ocio.GetCurrentConfig() - transform = self.__tf_type__.Identity() - - # Widgets - # Matrix - self.matrix_edit = FloatEditArray( - tuple([""] * 12), m44_to_m33(transform.getMatrix()), shape=(3, 3) - ) - self.matrix_edit.value_changed.connect(self._on_edit) - - self.offset_edit = FloatEditArray( - tuple([""] * 3), v4_to_v3(transform.getOffset()) - ) - self.offset_edit.value_changed.connect(self._on_edit) - - matrix_layout = FormLayout() - matrix_layout.addRow("Matrix", self.matrix_edit) - matrix_layout.addRow("Offset", self.offset_edit) - self.matrix_params = QtWidgets.QFrame() - self.matrix_params.setObjectName("matrix_transform_edit_matrix_params") - self.matrix_params.setLayout(matrix_layout) - self.matrix_params.setStyleSheet( - f"QFrame#matrix_transform_edit_matrix_params {{" - f" border-top: 1px solid " - f" {self.palette().color(QtGui.QPalette.Base).name()};" - f" padding-top: 8px;" - f"}}" - ) - - no_params = QtWidgets.QFrame() - no_params.setMaximumHeight(0) - - # Sat - self.sat_edit = FloatEdit(1.0) - self.sat_edit.value_changed.connect(self._on_edit) - - self.sat_luma_coef_edit = FloatEditArray(RGB, config.getDefaultLumaCoefs()) - self.sat_luma_coef_edit.value_changed.connect(self._on_edit) - - sat_layout = FormLayout() - sat_layout.addRow("Saturation", self.sat_edit) - sat_layout.addRow("Luma Coefficients", self.sat_luma_coef_edit) - self.sat_params = QtWidgets.QFrame() - self.sat_params.setLayout(sat_layout) - - # Scale - self.scale_edit = FloatEditArray(RGB, (1.0, 1.0, 1.0)) - self.scale_edit.value_changed.connect(self._on_edit) - - scale_layout = FormLayout() - scale_layout.addRow("Scale", self.scale_edit) - self.scale_params = QtWidgets.QFrame() - self.scale_params.setLayout(scale_layout) - - # View - self.channel_hot_edit = IntEditArray(RGBA, (1, 1, 1, 1)) - self.channel_hot_edit.value_changed.connect(self._on_edit) - - self.view_luma_coef_edit = FloatEditArray(RGB, config.getDefaultLumaCoefs()) - self.view_luma_coef_edit.value_changed.connect(self._on_edit) - - view_layout = FormLayout() - view_layout.addRow("Channel Hot", self.channel_hot_edit) - view_layout.addRow("Luma Coefficients", self.view_luma_coef_edit) - self.view_params = QtWidgets.QFrame() - self.view_params.setLayout(view_layout) - - self.params_stack = ExpandingStackedWidget() - self.params_stack.addWidget(no_params) - self.params_stack.addWidget(self.sat_params) - self.params_stack.addWidget(self.scale_params) - self.params_stack.addWidget(self.view_params) - - self.params_combo = ComboBox() - self.params_combo.addItems(["Matrix", "Saturation", "Scale", "View"]) - self.params_combo.currentIndexChanged.connect(self._on_edit) - - # Link parameter selection to parameter stack current index - self.params_combo.currentIndexChanged[int].connect( - self.params_stack.setCurrentIndex - ) - - # Layout - self.tf_layout.insertRow(0, self.matrix_params) - self.tf_layout.insertRow(0, self.params_stack) - self.tf_layout.insertRow(0, self.params_combo) - - def transform(self) -> ocio.ColorSpaceTransform: - params_choice = self.params_combo.currentText() - - if params_choice == "Saturation": - transform = self.__tf_type__.Sat( - self.sat_edit.value(), self.sat_luma_coef_edit.value() - ) - elif params_choice == "Scale": - transform = self.__tf_type__.Scale(v3_to_v4(self.scale_edit.value())) - elif params_choice == "View": - transform = self.__tf_type__.View( - self.channel_hot_edit.value(), self.view_luma_coef_edit.value() - ) - else: # Matrix - transform = self.create_transform() - transform.setMatrix(m33_to_m44(self.matrix_edit.value())) - transform.setOffset(v3_to_v4(self.offset_edit.value())) - - transform.setDirection(self.direction_combo.member()) - - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - self.matrix_edit.set_value(m44_to_m33(transform.getMatrix())) - self.offset_edit.set_value(v4_to_v3(transform.getOffset())) - - -TransformEditFactory.register(ocio.MatrixTransform, MatrixTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/range_edit.py b/src/apps/ocioview/ocioview/transforms/range_edit.py deleted file mode 100644 index c6f4d058b4..0000000000 --- a/src/apps/ocioview/ocioview/transforms/range_edit.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtWidgets - -from ..widgets import CheckBox, EnumComboBox, FloatEdit -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory - - -class RangeTransformEdit(BaseTransformEdit): - __icon_glyph__ = "mdi6.code-brackets" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Widgets - self.has_min_in = CheckBox("") - self.has_min_in.stateChanged.connect(self._on_toggle_state_changed) - self.has_min_in.stateChanged.connect(self._on_edit) - - self.min_in_edit = FloatEdit(0.0) - self.min_in_edit.value_changed.connect(self._on_edit) - - self.has_max_in = CheckBox("") - self.has_max_in.stateChanged.connect(self._on_toggle_state_changed) - self.has_max_in.stateChanged.connect(self._on_edit) - - self.max_in_edit = FloatEdit(1.0) - self.max_in_edit.value_changed.connect(self._on_edit) - - self.has_min_out = CheckBox("") - self.has_min_out.stateChanged.connect(self._on_toggle_state_changed) - self.has_min_out.stateChanged.connect(self._on_edit) - - self.min_out_edit = FloatEdit(0.0) - self.min_out_edit.value_changed.connect(self._on_edit) - - self.has_max_out = CheckBox("") - self.has_max_out.stateChanged.connect(self._on_toggle_state_changed) - self.has_max_out.stateChanged.connect(self._on_edit) - - self.max_out_edit = FloatEdit(1.0) - self.max_out_edit.value_changed.connect(self._on_edit) - - self.range_style_combo = EnumComboBox(ocio.RangeStyle) - self.range_style_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - min_in_layout = QtWidgets.QHBoxLayout() - min_in_layout.addWidget(self.has_min_in) - min_in_layout.setStretchFactor(self.has_min_in, 0) - min_in_layout.addWidget(self.min_in_edit) - min_in_layout.setStretchFactor(self.min_in_edit, 1) - - max_in_layout = QtWidgets.QHBoxLayout() - max_in_layout.addWidget(self.has_max_in) - max_in_layout.setStretchFactor(self.has_max_in, 0) - max_in_layout.addWidget(self.max_in_edit) - max_in_layout.setStretchFactor(self.max_in_edit, 1) - - min_out_layout = QtWidgets.QHBoxLayout() - min_out_layout.addWidget(self.has_min_out) - min_out_layout.setStretchFactor(self.has_min_out, 0) - min_out_layout.addWidget(self.min_out_edit) - min_out_layout.setStretchFactor(self.min_out_edit, 1) - - max_out_layout = QtWidgets.QHBoxLayout() - max_out_layout.addWidget(self.has_max_out) - max_out_layout.setStretchFactor(self.has_max_out, 0) - max_out_layout.addWidget(self.max_out_edit) - max_out_layout.setStretchFactor(self.max_out_edit, 1) - - self.tf_layout.insertRow(0, "Range Style", self.range_style_combo) - self.tf_layout.insertRow(0, "Max Out", max_out_layout) - self.tf_layout.insertRow(0, "Min Out", min_out_layout) - self.tf_layout.insertRow(0, "Max In", max_in_layout) - self.tf_layout.insertRow(0, "Min In", min_in_layout) - - self._on_toggle_state_changed(0) - - def transform(self) -> ocio.ColorSpaceTransform: - transform = super().transform() - if self.has_min_in.isChecked(): - transform.setMinInValue(self.min_in_edit.value()) - if self.has_max_in.isChecked(): - transform.setMaxInValue(self.max_in_edit.value()) - if self.has_min_out.isChecked(): - transform.setMinOutValue(self.min_out_edit.value()) - if self.has_max_out.isChecked(): - transform.setMaxOutValue(self.max_out_edit.value()) - transform.setStyle(self.range_style_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - super().update_from_transform(transform) - - has_min_in = transform.hasMinInValue() - self.has_min_in.setChecked(has_min_in) - if has_min_in: - self.min_in_edit.set_value(transform.getMinInValue()) - - has_max_in = transform.hasMaxInValue() - self.has_max_in.setChecked(has_max_in) - if has_max_in: - self.max_in_edit.set_value(transform.getMaxInValue()) - - has_min_out = transform.hasMinOutValue() - self.has_min_out.setChecked(has_min_out) - if has_min_out: - self.min_out_edit.set_value(transform.getMinOutValue()) - - has_max_out = transform.hasMaxOutValue() - self.has_max_out.setChecked(has_max_out) - if has_max_out: - self.max_out_edit.set_value(transform.getMaxOutValue()) - - self.range_style_combo.set_member(transform.getStyle()) - - @QtCore.Slot(int) - def _on_toggle_state_changed(self, index: int): - """ - Toggle range widget sections based on enabled bounds. - """ - self.min_in_edit.setEnabled(self.has_min_in.isChecked()) - self.max_in_edit.setEnabled(self.has_max_in.isChecked()) - self.min_out_edit.setEnabled(self.has_min_out.isChecked()) - self.max_out_edit.setEnabled(self.has_max_out.isChecked()) - - -TransformEditFactory.register(ocio.RangeTransform, RangeTransformEdit) diff --git a/src/apps/ocioview/ocioview/transforms/transform_edit.py b/src/apps/ocioview/ocioview/transforms/transform_edit.py deleted file mode 100644 index 99a3cc021c..0000000000 --- a/src/apps/ocioview/ocioview/transforms/transform_edit.py +++ /dev/null @@ -1,259 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -from functools import partial -from typing import Optional - -import PyOpenColorIO as ocio -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 ..utils import get_glyph_icon, item_type_label -from ..widgets import EnumComboBox, FormLayout - - -class BaseTransformEdit(QtWidgets.QFrame): - """ - Base widget for editing an OCIO transform instance. - """ - - # Transform type this widget edits, set on registration with - # TransformEditFactory. - __tf_type__: type = ocio.Transform - - # Transform type label for use in GUI components, generated on first call to - # ``transform_type_label()``. - __tf_type_label__: str = None - - # QtAwesome glyph name to use for this transform type's icon - __icon_glyph__: str = None - - # Transform type icon, loaded on first call to ``transform_type_icon()``. - __icon__: QtGui.QIcon = None - - edited = QtCore.Signal() - moved_up = QtCore.Signal(QtWidgets.QWidget) - moved_down = QtCore.Signal(QtWidgets.QWidget) - deleted = QtCore.Signal(QtWidgets.QWidget) - - @classmethod - 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 - ) - return cls.__icon__ - - @classmethod - def transform_type_label(cls) -> str: - """ - :return: Friendly type name - """ - if cls.__tf_type_label__ is None: - # Remove trailing "Transform" token - cls.__tf_type_label__ = item_type_label(cls.__tf_type__).rsplit( - " ", 1 - )[0] - return cls.__tf_type_label__ - - @classmethod - def create_transform(cls, *args, **kwargs) -> ocio.Transform: - """ - Create a new transform with passthrough constructor - args/kwargs. - - :return: Transform instance - """ - return cls.__tf_type__(*args, **kwargs) - - @classmethod - def from_transform(cls, transform: ocio.Transform) -> BaseTransformEdit: - """ - Create and populate a transform edit from an existing transform - instance. - - :param transform: Transform to create edit widget for - :return: Transform edit - """ - tf_edit = cls() - tf_edit.update_from_transform(transform) - return tf_edit - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - palette = self.palette() - - self.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.setObjectName("transform_edit") - apply_widget_with_top_tool_bar_style(self) - - # Widgets - self._expand_icon = get_glyph_icon("ph.caret-right") - self._collapse_icon = get_glyph_icon("ph.caret-down") - - self.icon_label = None - if self.__icon_glyph__ is not None: - self.icon_label = get_glyph_icon( - self.__icon_glyph__, as_widget=True - ) - - self.expand_button = QtWidgets.QToolButton() - self.expand_button.setIcon(self._collapse_icon) - self.expand_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.expand_button.released.connect(self._on_expand_button_released) - - self.move_up_button = QtWidgets.QToolButton() - self.move_up_button.setIcon(get_glyph_icon("ph.arrow-up")) - self.move_up_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.move_up_button.released.connect(partial(self.moved_up.emit, self)) - - 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.delete_button = QtWidgets.QToolButton() - self.delete_button.setIcon(get_glyph_icon("ph.x")) - self.delete_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.delete_button.released.connect(partial(self.deleted.emit, self)) - - tool_bar_left = QtWidgets.QToolBar() - tool_bar_left.setContentsMargins(0, 0, 0, 0) - tool_bar_left.setIconSize(ICON_SIZE_ITEM) - if self.icon_label is not None: - tool_bar_left.addWidget(self.icon_label) - tool_bar_left.addWidget(self.expand_button) - - tool_bar_right = QtWidgets.QToolBar() - tool_bar_right.setContentsMargins(0, 0, 0, 0) - tool_bar_right.setIconSize(ICON_SIZE_ITEM) - tool_bar_right.addWidget(self.move_up_button) - tool_bar_right.addWidget(self.move_down_button) - tool_bar_right.addWidget(self.delete_button) - - self.direction_combo = EnumComboBox(ocio.TransformDirection) - self.direction_combo.currentIndexChanged.connect(self._on_edit) - - # Layout - self.tf_layout = FormLayout() - self.tf_layout.setContentsMargins(12, 12, 12, 12) - self.tf_layout.setLabelAlignment(QtCore.Qt.AlignRight) - self.tf_layout.addRow("Direction", self.direction_combo) - - self._tf_frame = QtWidgets.QFrame() - self._tf_frame.setObjectName("transform_edit__tf_frame") - self._tf_frame.setStyleSheet( - f"QFrame#transform_edit__tf_frame {{" - f" border-top: 1px solid {palette.color(BORDER_COLOR_ROLE).name()};" - f"}}" - ) - self._tf_frame.setLayout(self.tf_layout) - - header_layout = QtWidgets.QHBoxLayout() - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.setSpacing(0) - header_layout.addWidget(tool_bar_left) - header_layout.addWidget(QtWidgets.QLabel(self.transform_type_label())) - header_layout.addStretch() - header_layout.addWidget(tool_bar_right) - - self._header_frame = QtWidgets.QFrame() - self._header_frame.setObjectName("transform_edit__header_frame") - self._header_frame.setLayout(header_layout) - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(self._header_frame) - layout.addWidget(self._tf_frame) - - self.setLayout(layout) - - # Initialize - self._update_state() - - def transform(self) -> ocio.Transform: - """ - Create a new transform from the current widget values. - Subclasses must extend this method to account for all transform - parameters. - - :return: New transform - """ - transform = self.create_transform() - transform.setDirection(self.direction_combo.member()) - return transform - - def update_from_transform(self, transform: ocio.Transform) -> None: - """ - Update an existing transform from the current widget values. - Subclasses must extend this method to account for all transform - parameters. - - :param transform: Transform to update - """ - self.direction_combo.set_member(transform.getDirection()) - - def update_from_config(self) -> None: - """ - Subclasses must update widget state from the current config, - which could change widget options or data (e.g. available color - spaces). - """ - pass - - def set_collapsed(self, collapsed: bool) -> None: - """ - Set the widget's collapsed state, to show or hide the - transform parameter widgets. - - :param collapsed: Collapsed state - """ - self._tf_frame.setHidden(collapsed) - self._update_state() - - def _on_edit(self, *args, **kwargs) -> None: - """ - Subclasses must connect all widget modified signals to this - slot to notify the application of changes affecting the current - config. - """ - self.edited.emit() - - def _on_expand_button_released(self) -> None: - """ - Hide transform parameter widgets to toggle the widget's - collapsed state. - """ - if self._tf_frame.isHidden(): - self._tf_frame.setHidden(False) - else: - self._tf_frame.setHidden(True) - self._update_state() - - def _update_state(self) -> None: - """ - Restyle widget to toggle its collapsed state. - """ - if self._tf_frame.isHidden(): - self.expand_button.setIcon(self._expand_icon) - 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 - ) diff --git a/src/apps/ocioview/ocioview/transforms/transform_edit_factory.py b/src/apps/ocioview/ocioview/transforms/transform_edit_factory.py deleted file mode 100644 index 1ac5540eff..0000000000 --- a/src/apps/ocioview/ocioview/transforms/transform_edit_factory.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import PyOpenColorIO as ocio - -from .transform_edit import BaseTransformEdit -from .utils import ravel_transform - - -class TransformEditFactory: - """ - Factory interface for registering and creating transform edits for - implemented transform types. - """ - - _registry = {} - - @classmethod - def transform_edit_type(cls, transform_type: type) -> type: - """ - Get the transform edit type for the specified transform type. - - :param transform_type: Transform type - :return: Transform edit type - """ - if transform_type not in cls._registry: - raise TypeError(f"Unsupported transform type: {transform_type.__name__}") - - return cls._registry[transform_type] - - @classmethod - def from_transform_type(cls, transform_type: type) -> BaseTransformEdit: - """ - Create a transform edit for the specified transform type. - - :param transform_type: Transform type - :return: Transform edit widget - """ - return cls.transform_edit_type(transform_type)() - - @classmethod - def from_transform(cls, transform: ocio.Transform) -> BaseTransformEdit: - """ - Create a transform edit from an existing transform instance. - - :param transform: Transform instance - :return: Transform edit widget - """ - tf_type = transform.__class__ - return cls.transform_edit_type(tf_type).from_transform(transform) - - @classmethod - def from_transform_recursive( - cls, transform: ocio.GroupTransform - ) -> list[BaseTransformEdit]: - """ - Recursively ravel group transform instance into flattened list - of transform edits. - - :param transform: Group transform instance - :return: list of transform edit widgets - """ - return [cls.from_transform(tf) for tf in ravel_transform(transform)] - - @classmethod - def transform_types(cls) -> list[type]: - """ - list all registered transform types. - - :return: list of transform types - """ - return sorted(cls._registry.keys(), key=lambda t: t.__name__) - - @classmethod - def register(cls, tf_type: type, tf_edit_type: type) -> None: - """ - All subclasses of BaseTransformEdit must be registered with - this method. - - :param tf_type: ocio.Transform subclass - :param tf_edit_type: TransformEdit type - """ - if tf_type != ocio.Transform: - # Store transform type on the edit class - tf_edit_type.__tf_type__ = tf_type - cls._registry[tf_type] = tf_edit_type diff --git a/src/apps/ocioview/ocioview/transforms/transform_edit_stack.py b/src/apps/ocioview/ocioview/transforms/transform_edit_stack.py deleted file mode 100644 index 1f7eaf10b6..0000000000 --- a/src/apps/ocioview/ocioview/transforms/transform_edit_stack.py +++ /dev/null @@ -1,352 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from functools import partial -from typing import Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets - -# Register all transform edit types -from .. import transforms - -from ..constants import ICON_SIZE_BUTTON, MARGIN_WIDTH -from ..utils import get_glyph_icon -from .transform_edit import BaseTransformEdit -from .transform_edit_factory import TransformEditFactory -from .utils import ravel_transform - - -class TransformEditStack(QtWidgets.QWidget): - """ - Widget that composes one or more transform edits to reflect the - transforms for an object in the config in a single direction. - """ - - edited = QtCore.Signal() - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - # Fits widest transform edit (Exponent[WithLinear]Transform) - self.setMinimumWidth(500) - - # Widgets - self.tf_menu = QtWidgets.QMenu() - for tf_type in TransformEditFactory.transform_types(): - tf_edit_type = TransformEditFactory.transform_edit_type(tf_type) - self.tf_menu.addAction( - tf_edit_type.transform_type_icon(), - tf_edit_type.transform_type_label(), - lambda t=tf_type: self._create_transform_edit(t), - ) - - self.add_tf_button = QtWidgets.QToolButton() - self.add_tf_button.setText(" ") - self.add_tf_button.setIcon(get_glyph_icon("ph.plus")) - self.add_tf_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.add_tf_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) - self.add_tf_button.setMenu(self.tf_menu) - - self._start_collapsed_action = QtGui.QAction("Start Collapsed") - self._start_collapsed_action.setCheckable(True) - self._start_collapsed_action.triggered[bool].connect( - self._on_start_collapsed_changed - ) - - self.settings_menu = QtWidgets.QMenu() - self.settings_menu.addAction(self._start_collapsed_action) - - settings_button = QtWidgets.QToolButton() - settings_button.setText(" ") - settings_button.setIcon(get_glyph_icon("ph.gear")) - settings_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) - settings_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) - settings_button.setMenu(self.settings_menu) - - tool_bar = QtWidgets.QToolBar() - tool_bar.setStyleSheet( - "QToolButton::menu-indicator {" - " subcontrol-position: center right;" - " right: 7px;" - "}" - ) - tool_bar.setIconSize(ICON_SIZE_BUTTON) - tool_bar.addWidget(self.add_tf_button) - tool_bar.addWidget(settings_button) - - self.tf_info_label = QtWidgets.QLabel("") - - self.tf_layout = QtWidgets.QVBoxLayout() - self.tf_layout.addStretch() - tf_frame = QtWidgets.QWidget() - tf_frame.setLayout(self.tf_layout) - - tf_scroll_area = QtWidgets.QScrollArea() - tf_scroll_area.setObjectName("transform_edit_stack_scroll_area") - tf_scroll_area.setSizePolicy( - QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding - ) - tf_scroll_area.setWidgetResizable(True) - tf_scroll_area.setStyleSheet( - f"QScrollArea#transform_edit_stack_scroll_area {{" - f" border: none;" - f" border-top: 1px solid " - f" {self.palette().color(QtGui.QPalette.Dark).name()};" - f"}}" - ) - tf_scroll_area.setWidget(tf_frame) - - # Layout - top_layout = QtWidgets.QHBoxLayout() - top_layout.setContentsMargins(0, 0, MARGIN_WIDTH, 0) - top_layout.addWidget(tool_bar) - top_layout.addStretch() - top_layout.addWidget(self.tf_info_label) - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addLayout(top_layout) - layout.addWidget(tf_scroll_area) - - self.setLayout(layout) - - # Initialize - self._update_transform_info_label() - - def reset(self) -> None: - """ - Clear all transforms. - """ - self.set_transform(None) - - def transform(self) -> Optional[ocio.Transform]: - """ - Compose the stack into a single transform and return it. If - there are multiple transform edits, this will be a group - transform, otherwise it will be a single transform or None, for - one or zero transform edits respectively. - - :return: Composed transform - """ - tfs = [] - for i in range(self.transform_count()): - item = self.tf_layout.itemAt(i) - widget = item.widget() - if widget: - if isinstance(widget, BaseTransformEdit): - tfs.append(widget.transform()) - - tf_count = len(tfs) - if not tf_count: - return None - elif tf_count == 1: - return tfs[0] - else: - # Adding transforms in the constructor will validate all transforms, which - # is avoided here since transforms may be in an intermediate state while - # being edited. - group_tf = ocio.GroupTransform() - for tf in tfs: - group_tf.appendTransform(tf) - return group_tf - - def set_transform(self, transform: Optional[ocio.Transform]) -> None: - """ - Reinitialize stack from a transform. Group transforms will be - raveled recursively into a flattened list of transform edits. - - :param transform: Transform to decompose - """ - start_collapsed = self._start_collapsed_action.isChecked() - - if transform is not None: - tfs = ravel_transform(transform) - else: - tfs = [] - - # Do transforms match current widgets? - tf_count = len(tfs) - tf_edits = self.transform_edits() - - if tf_count == len(tf_edits): - if [tf.__class__ for tf in tfs] == [ - tf_edit.__tf_type__ for tf_edit in tf_edits - ]: - # Update transform widgets - for i in range(tf_count): - tf_edits[i].update_from_transform(tfs[i]) - return - - # Rebuild transforms - self._clear_transform_layout() - - for tf in tfs: - tf_edit = TransformEditFactory.from_transform(tf) - tf_edit.set_collapsed(start_collapsed) - self._connect_signals(tf_edit) - self.tf_layout.addWidget(tf_edit) - self.tf_layout.addStretch() - - self._on_transforms_changed() - - def transform_count(self) -> int: - """ - :return: Number of transforms in stack - """ - # -1 to account for spacer item beneath transform edits - return self.tf_layout.count() - 1 - - def transform_edits(self) -> list[BaseTransformEdit]: - """ - :return: list of transform edits in stack - """ - tf_edits = [] - for i in range(self.transform_count()): - item = self.tf_layout.itemAt(i) - widget = item.widget() - if widget and isinstance(widget, BaseTransformEdit): - tf_edits.append(widget) - return tf_edits - - def _create_transform_edit(self, transform_type: type) -> None: - """ - Create a new transform edit from a transform type. - - :param transform_type: Transform class - """ - tf_edit = TransformEditFactory.from_transform_type(transform_type) - tf_edit.set_collapsed(self._start_collapsed_action.isChecked()) - self._connect_signals(tf_edit) - self._insert_transform_edit(self.transform_count(), tf_edit) - - def _insert_transform_edit( - self, index: int, transform_edit: BaseTransformEdit - ) -> None: - """ - Insert transform edit in the stack at the specified index. - - :param index: Stack index - :param transform_edit: Transform edit to insert - """ - self.tf_layout.insertWidget(index, transform_edit) - transform_edit.show() - - self._on_transforms_changed() - - def _pop_transform_edit(self, transform_edit: BaseTransformEdit) -> int: - """ - Remove the specified transform edit from the stack and return - its index. - - :param transform_edit: Transform edit to remove - :return: Stack index - """ - for i in range(self.tf_layout.count()): - item = self.tf_layout.itemAt(i) - widget = item.widget() - - if widget == transform_edit: - # Hide the widget before unparenting to prevent it from being - # raised in its own window. - widget.hide() - widget.setParent(None) - return i - - # If widget was not in layout, assume it was at the bottom - return self.transform_count() - - def _clear_transform_layout(self) -> None: - """ - Remove all transform edits from stack. - """ - for i in reversed(range(self.tf_layout.count())): - item = self.tf_layout.takeAt(i) - widget = item.widget() - - if widget: - # Hide the widget before unparenting to prevent it from being - # raised in its own window. - widget.hide() - widget.setParent(None) - widget.deleteLater() - - self._on_transforms_changed() - - @QtCore.Slot(QtWidgets.QWidget) - def _on_transform_edit_deleted(self, transform_edit: BaseTransformEdit) -> None: - """ - Remove and delete the specified transform edit. - - :param transform_edit: Transform edit to remove - """ - self._pop_transform_edit(transform_edit) - transform_edit.deleteLater() - - self._on_transforms_changed() - - @QtCore.Slot(QtWidgets.QWidget) - def _on_transform_edit_moved( - self, move: int, transform_edit: BaseTransformEdit - ) -> None: - """ - Offset the specified transform edit index by a signed 'move' - value. - - :param move: Signed index offset - :param transform_edit: Transform edit to move - """ - tf_count = self.transform_count() - - # Can a transform be moved? - if tf_count <= 1: - return - - prev_index = self._pop_transform_edit(transform_edit) - - self._insert_transform_edit( - # Clamp between negative index (which will not insert the widget), - # and spacer item index (last item). - min(tf_count - 1, max(0, prev_index + move)), - transform_edit, - ) - - @QtCore.Slot(bool) - def _on_start_collapsed_changed(self, checked: bool) -> None: - """ - Update collapsed state upon changing the "Start Collapsed" - setting. - """ - for tf_edit in self.transform_edits(): - tf_edit.set_collapsed(checked) - - def _on_transforms_changed(self) -> None: - """ - Notify the application that transforms in the stack have - changed. - """ - self.edited.emit() - self._update_transform_info_label() - - def _connect_signals(self, transform_edit: BaseTransformEdit) -> None: - """ - Connect transform edit signals to stack. This facilitates - transform edit move and delete buttons, and notifications to - the application of changed transform parameters. - - :param transform_edit: Transform edit to connect - """ - transform_edit.edited.connect(self.edited.emit) - transform_edit.deleted.connect(self._on_transform_edit_deleted) - transform_edit.moved_up.connect(partial(self._on_transform_edit_moved, -1)) - transform_edit.moved_down.connect(partial(self._on_transform_edit_moved, 1)) - - def _update_transform_info_label(self) -> None: - """ - Update transform info label to list the current transform - count. - """ - tf_count = self.transform_count() - self.tf_info_label.setText( - f"{tf_count} transform{'s' if tf_count != 1 else ''}" - ) diff --git a/src/apps/ocioview/ocioview/transforms/utils.py b/src/apps/ocioview/ocioview/transforms/utils.py deleted file mode 100644 index ed112713a1..0000000000 --- a/src/apps/ocioview/ocioview/transforms/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import PyOpenColorIO as ocio - - -def ravel_transform(transform: ocio.Transform) -> list[ocio.Transform]: - """ - Recursively ravel group transform into flattened list of - transforms. Other transform types are returned as the sole list - item. - - :param transform: Transform to ravel - :return: list of transforms - """ - transforms = [] - - def ravel_recursive(tf): - if isinstance(tf, ocio.GroupTransform): - for child in tf: - ravel_recursive(child) - else: - transforms.append(tf) - - if transform is not None: - ravel_recursive(transform) - - return transforms diff --git a/src/apps/ocioview/ocioview/undo.py b/src/apps/ocioview/ocioview/undo.py deleted file mode 100644 index d8b0d14f96..0000000000 --- a/src/apps/ocioview/ocioview/undo.py +++ /dev/null @@ -1,206 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import warnings -from types import TracebackType -from typing import Any, Optional - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui - -from .config_cache import ConfigCache - - -undo_stack = QtGui.QUndoStack() -"""Global undo stack.""" - - -class ItemModelUndoCommand(QtGui.QUndoCommand): - """ - Undo command for use in item model ``setData`` implementations. - - .. note:: - These undo command implementations add themselves to the undo - stack. Upon being added to the stack, their ``redo`` method - will be called automatically. - """ - - def __init__( - self, - text: str, - index: QtCore.QPersistentModelIndex, - redo_value: Any, - undo_value: Any, - parent: Optional[QtGui.QUndoCommand] = None, - ): - """ - :param text: Undo/redo command menu text - :param index: Persistent index of the modified item - :param redo_value: Value to set initially on redo - :param undo_value: Value to set on undo - """ - super().__init__(text, parent=parent) - - self._index = index - self._redo_value = redo_value - self._undo_value = undo_value - - # Add self to undo stack - undo_stack.push(self) - - def redo(self) -> None: - if self._index.isValid(): - model = self._index.model() - model.setData(self._index, self._redo_value) - - def undo(self) -> None: - if self._index.isValid(): - model = self._index.model() - model.setData(self._index, self._undo_value) - - -class ConfigSnapshotUndoCommand(QtGui.QUndoCommand): - """ - Undo command for complex config changes like item adds, moves, - and deletes, to be used as a content manager in which the entry - state is the undo state and the exit state is the redo state. - """ - - def __init__( - self, - text: str, - model: Optional[QtCore.QAbstractItemModel] = None, - item_name: Optional[str] = None, - parent: Optional[QtGui.QUndoCommand] = None, - ): - """ - :param text: Undo/redo command menu text - :param model: Model to be reset on applied undo/redo - :param item_name: Optional item name to try and select in the - optionally provided model upon applying undo/redo. - """ - super().__init__(text, parent=parent) - - self._model = model - self._item_name = item_name - self._undo_cache_id = None - self._undo_state = None - self._redo_cache_id = None - self._redo_state = None - - # Since this undo command captures config changes within its managed context, - # redo should not be called upon adding the command to the undo stack. - self._init_command = True - - def __enter__(self) -> None: - config = ocio.GetCurrentConfig() - self._undo_cache_id, is_valid = ConfigCache.get_cache_id() - if is_valid: - self._undo_state = config.serialize() - - def __exit__( - self, exc_type: type, exc_val: Exception, exc_tb: TracebackType - ) -> None: - config = ocio.GetCurrentConfig() - self._redo_cache_id, is_valid = ConfigCache.get_cache_id() - if is_valid: - self._redo_state = config.serialize() - - # Add self to undo stack if config state could be captured - if self._undo_state is not None and self._redo_state is not None: - undo_stack.push(self) - # Enable redo now that the command is in the stack - self._init_command = False - else: - state_desc = [] - if self._undo_state is None: - state_desc.append("starting") - elif self._redo_state is None: - state_desc.append("ending") - - warnings.warn( - f"Command '{self.text()}' could not be added to the undo stack " - f"because its {' and '.join(state_desc)} config " - f"state{'s are' if len(state_desc) == 2 else ' is'} invalid." - ) - - def _apply_state(self, cache_id, state): - """ - :param cache_id: Config cache ID to check. If this differs from - the current config's cache ID, the provided config state - will be restored. - :param state: Serialized config data to restore - """ - prev_item_names = [] - has_item_names = False - new_cache_id = ConfigCache.get_cache_id() - - if new_cache_id != cache_id: - if self._model is not None: - self._model.beginResetModel() - - # Get current item names in the model - if ( - hasattr(self._model, "get_item_names") - and hasattr(self._model, "get_index_from_item_name") - and hasattr(self._model, "item_selection_requested") - ): - prev_item_names = self._model.get_item_names() - has_item_names = True - - updated_config = ocio.Config.CreateFromStream(state) - ocio.SetCurrentConfig(updated_config) - - if self._model is not None: - self._model.endResetModel() - - if has_item_names: - next_item_names = self._model.get_item_names() - - # Try to select a requested item name - if ( - self._item_name is not None - and self._item_name in next_item_names - ): - index = self._model.get_index_from_item_name(self._item_name) - if index is not None: - self._model.item_selection_requested.emit(index) - return - - # Try to select the first added item, if any - added_item_names = set(next_item_names).difference(prev_item_names) - if added_item_names: - for item_name in sorted(added_item_names): - index = self._model.get_index_from_item_name(item_name) - if index is not None: - self._model.item_selection_requested.emit(index) - return - - # Try to select the first changed/reordered item, if any - elif ( - len(next_item_names) == len(prev_item_names) - and next_item_names != prev_item_names - ): - for next_item_name, prev_item_name in zip( - next_item_names, prev_item_names - ): - if next_item_name != prev_item_name: - index = self._model.get_index_from_item_name( - next_item_name - ) - if index is not None: - self._model.item_selection_requested.emit(index) - return - - # Fallback to selecting first item - column = 0 - if hasattr(self._model, "NAME"): - column = self._model.NAME.column - self._model.item_selection_requested.emit(self._model.index(0, column)) - - def redo(self) -> None: - if not self._init_command: - self._apply_state(self._redo_cache_id, self._redo_state) - - def undo(self) -> None: - self._apply_state(self._undo_cache_id, self._undo_state) diff --git a/src/apps/ocioview/ocioview/utils.py b/src/apps/ocioview/ocioview/utils.py deleted file mode 100644 index d1b1c27042..0000000000 --- a/src/apps/ocioview/ocioview/utils.py +++ /dev/null @@ -1,328 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import enum -import re -from pathlib import Path -from types import TracebackType -from typing import Optional, Union - -import PyOpenColorIO as ocio -import numpy as np -import qtawesome -from colour import RGB_Colourspace, XYZ_to_xy -from pygments import highlight -from pygments.lexers import GLShaderLexer, HLSLShaderLexer, XmlLexer, YamlLexer -from pygments.formatters import HtmlFormatter -from PySide6 import QtCore, QtGui, QtWidgets - -from .config_cache import ConfigCache -from .constants import ICON_SCALE_FACTOR, ICON_SIZE_BUTTON - - -class SignalsBlocked: - """ - Context manager which blocks signals to supplied QObjects during - execution of the contained scope. - """ - - def __init__(self, *args: QtCore.QObject): - self.objects = list(args) - - def __enter__(self) -> None: - for obj in self.objects: - obj.blockSignals(True) - - def __exit__( - self, exc_type: type, exc_val: Exception, exc_tb: TracebackType - ) -> None: - for obj in self.objects: - obj.blockSignals(False) - - -def get_glyph_icon( - name: str, - scale_factor: float = ICON_SCALE_FACTOR, - color: Optional[QtGui.QColor] = None, - as_widget: bool = False, - size: QtCore.QSize = ICON_SIZE_BUTTON, -) -> Union[QtGui.QIcon, QtWidgets.QLabel]: - """ - Get named glyph QIcon from QtAwesome. - - :param name: Glyph name - :param scale_factor: Amount to scale icon - :param color: Optional icon color override - :param as_widget: Set to True to return a widget displaying the - icon instead of a QIcon. - :param size: Override icon size - :return: Glyph QIcon or QLabel - """ - kwargs = {"scale_factor": scale_factor} - if color is not None: - kwargs["color"] = color - - icon = qtawesome.icon(name, **kwargs) - - if as_widget: - widget = QtWidgets.QLabel() - widget.setPixmap(icon.pixmap(size)) - return widget - else: - return icon - - -def get_icon( - icon_path: Path, rotate: float = 0.0, as_pixmap: bool = False -) -> QtGui.QIcon: - """ - Get QIcon or QPixmap from an icon path. - - :param icon_path: Icon file path - :param rotate: Optional degrees to rotate icon - :param as_pixmap: Whether to return a QPixmap instead of a QIcon - :return: QIcon or QPixmap - """ - # Rotate icon? - if rotate: - xform = QtGui.QTransform() - xform.rotate(rotate) - pixmap = QtGui.QPixmap(str(icon_path)) - pixmap = pixmap.transformed(xform) - if as_pixmap: - return pixmap - else: - return QtGui.QIcon(pixmap) - else: - return [QtGui.QIcon, QtGui.QPixmap][int(as_pixmap)](str(icon_path)) - - -def get_enum_member(enum_type: enum.Enum, value: int) -> Optional[enum.Enum]: - """ - Lookup an enum member from its type and value. - - :param enum_type: Enum type - :param value: Enum value - :return: Enum member or None if value not found - """ - for member in enum_type.__members__.values(): - if member.value == value: - return member - return None - - -def next_name(prefix: str, all_names: list[str]) -> str: - """ - Increment a name with the provided prefix and a number suffix until - it is unique in the given list of names. - - :param prefix: Name prefix, typically followed by an underscore - :param all_names: All sibling names which a new name cannot - intersect. - :return: Unique name - """ - lower_names = [name.lower() for name in all_names] - i = 1 - name = prefix + str(i) - while name.lower() in lower_names: - i += 1 - name = prefix + str(i) - return name - - -def item_type_label(item_type: type) -> str: - """ - Convert a config item type to a friendly type name - (e.g. "ColorSpace" -> "Color Space"). - - :param item_type: Config item type - :return: Friendly type name - """ - return " ".join( - filter(None, re.split(r"([A-Z]+[a-z]+)", item_type.__name__)) - ) - - -def m44_to_m33(m44: list) -> list: - """ - Convert list with 16 elements representing a 4x4 matrix to a list - with 9 elements representing a 3x3 matrix. - """ - return [*m44[0:3], *m44[4:7], *m44[8:11]] - - -def m33_to_m44(m33: list) -> list: - """ - Convert list with 9 elements representing a 3x3 matrix to a list - with 16 elements representing a 4x4 matrix. - """ - return [*m33[0:3], 0, *m33[3:6], 0, *m33[6:9], 0, 0, 0, 0, 1] - - -def v4_to_v3(v4: list) -> list: - """ - Convert list with 4 elements representing an XYZW or RGBA vector to - a list with 3 elements representing an XYZ or RGB vector. - """ - return v4[:3] - - -def v3_to_v4(v3: list) -> list: - """ - Convert list with 3 elements representing an XYZ or RGB vector to - a list with 4 elements representing an XYZW or RGBA vector. - """ - return [*v3, 0] - - -def config_to_html(config: ocio.Config) -> str: - """Return OCIO config formatted as HTML.""" - yaml_data = str(config) - return increase_html_lineno_padding( - highlight(yaml_data, YamlLexer(), HtmlFormatter(linenos="inline")) - ) - - -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() - - # Replace LUTs with identity LUTs since formatting and printing LUT data - # is expensive and unnecessary unless we're exporting CTF data to a file. - clean_group_tf = ocio.GroupTransform() - for tf in group_tf: - if isinstance(tf, ocio.Lut1DTransform): - clean_tf = ocio.Lut1DTransform() - elif isinstance(tf, ocio.Lut3DTransform): - clean_tf = ocio.Lut3DTransform() - else: - clean_tf = tf - clean_group_tf.appendTransform(clean_tf) - - ctf_data = clean_group_tf.write("Color Transform Format", config) - - # Inject ellipses into LUT elements to indicate to viewers that the data - # is truncated. - ctf_data = re.sub( - r"()", - r"\1>...[Export CTF to include LUT]...\2", - ctf_data, - ) - - return ( - increase_html_lineno_padding( - highlight(ctf_data, XmlLexer(), HtmlFormatter(linenos="inline")) - ), - group_tf, - ) - - -def processor_to_shader_html( - gpu_proc: ocio.GPUProcessor, gpu_language: ocio.GPU_LANGUAGE_GLSL_4_0 -) -> str: - """ - Return processor shader in the requested language, formatted as - HTML. - """ - 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 - )(), - HtmlFormatter(linenos="inline"), - ) - ) - - -def increase_html_lineno_padding(html: str) -> str: - """ - Adds two non-breaking spaces to the right of all line numbers - for some breathing room around the code. - """ - # This works with inline and table linenos - return re.sub( - r"(\s*)([0-9]+)()", - r"\1\2  \3", - html, - ) - - -def float_to_uint8(value: float) -> int: - """ - Convert float value to an 8-bit clamped unsigned integer value. - - :param value: Float value - :return: Integer value - """ - return max(0, min(255, int(value * 255))) - - -def subsampling_factor(a: np.ndarray, maximum_size: float) -> int: - """ - Return the best factor to sub-sample given :math:`a` array and have its - size less or equal to given maximum size. - - :param a: Array :math:`a` to find the best sub-sample factor. - :param maximum_size: Maximum size of the sub-sampled array :math:`a`. - :return: Sub-sampling factor. - """ - - size = a.size - - sub_sampling_factor = 1 - while True: - if size / sub_sampling_factor <= maximum_size: - return sub_sampling_factor - - sub_sampling_factor += 1 - - -def color_space_to_rgb_colourspace(color_space: str) -> RGB_Colourspace | None: - """ - Convert a color space name from the current Config to a - :class:`RGB_Colourspace` class instance. - - :param color_space: Color space name from the current Config. - :return: :class:`RGB_Colourspace` class instance. - """ - - config = ocio.GetCurrentConfig() - if (not config.hasRole(ocio.ROLE_INTERCHANGE_DISPLAY)) or ( - color_space not in ConfigCache.get_color_space_names() - ): - return None - - colorspace_transform = ocio.ColorSpaceTransform( - src=color_space, - dst=ocio.ROLE_INTERCHANGE_DISPLAY, - ) - processor = config.getProcessor( - colorspace_transform, ocio.TRANSFORM_DIR_FORWARD - ).getDefaultCPUProcessor() - - XYZ = np.identity(3, dtype=np.float32) - processor.applyRGB(XYZ) - - XYZ_w = np.ones(3, dtype=np.float32) - processor.applyRGB(XYZ_w) - - return RGB_Colourspace( - color_space, - XYZ_to_xy(XYZ), - XYZ_to_xy(XYZ_w), - color_space, - use_derived_matrix_RGB_to_XYZ=True, - use_derived_matrix_XYZ_to_RGB=True, - ) diff --git a/src/apps/ocioview/ocioview/viewer/__init__.py b/src/apps/ocioview/ocioview/viewer/__init__.py deleted file mode 100644 index 7784469072..0000000000 --- a/src/apps/ocioview/ocioview/viewer/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from .image_viewer import ViewerChannels, ImageViewer -from .offscreen_viewer import WgpuCanvasOffScreenViewer -from .utils import load_image diff --git a/src/apps/ocioview/ocioview/viewer/image_plane.py b/src/apps/ocioview/ocioview/viewer/image_plane.py deleted file mode 100644 index 5117934ada..0000000000 --- a/src/apps/ocioview/ocioview/viewer/image_plane.py +++ /dev/null @@ -1,1281 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -# TODO: Much of the OpenGL code in this module is adapted from the -# oglapphelpers library bundled with OCIO. We should fully -# reimplement that in Python for direct use in applications. - -from __future__ import annotations - -import ctypes -import logging -import math -from functools import partial -from pathlib import Path -from typing import Any, Optional - -import numpy as np -from OpenGL import GL - -import PyOpenColorIO as ocio -from PySide6 import QtCore, QtGui, QtWidgets, QtOpenGLWidgets - -from ..log_handlers import message_queue -from ..processor_context import ProcessorContext -from ..ref_space_manager import ReferenceSpaceManager -from .utils import load_image - - -logger = logging.getLogger(__name__) - - -GLSL_VERT_SRC = """#version 400 core - -uniform mat4 mvpMat; -in vec3 in_position; -in vec2 in_texCoord; - -out vec2 vert_texCoord; - -void main() { - vert_texCoord = in_texCoord; - gl_Position = mvpMat * vec4(in_position, 1.0); -} - -""" -""" -Simple vertex shader which transforms all vertices with a -model-view-projection matrix uniform. -""" - -GLSL_FRAG_SRC = """#version 400 core - -uniform sampler2D imageTex; -in vec2 vert_texCoord; - -out vec4 frag_color; - -void main() {{ - frag_color = texture(imageTex, vert_texCoord); -}} -""" -""" -Simple fragment shader which performs a 2D texture lookup to map an -image texture onto UVs. This is used when OCIO is unavailable, like -before its shader initialization. -""" - -GLSL_FRAG_OCIO_SRC_FMT = """#version 400 core - -uniform sampler2D imageTex; -in vec2 vert_texCoord; - -out vec4 frag_color; - -{ocio_src} - -void main() {{ - vec4 inColor = texture(imageTex, vert_texCoord); - vec4 outColor = OCIOMain(inColor); - frag_color = outColor; -}} -""" -""" -Fragment shader which performs a 2D texture lookup to map an image -texture onto UVs and processes fragments through an OCIO-provided -shader program segment, which itself utilizes additional texture -lookups, dynamic property uniforms, and various native GLSL op -implementations. Note that this shader's cost will increase with -additional LUTs in an OCIO processor, since each adds its own -2D or 3D texture. -""" - - -class ImagePlane(QtOpenGLWidgets.QOpenGLWidget): - """ - Qt-wrapped OpenGL window for drawing with PyOpenGL. - """ - - image_loaded = QtCore.Signal(Path, int, int) - sample_changed = QtCore.Signal( - int, int, float, float, float, float, float, float - ) - scale_changed = QtCore.Signal(float) - tf_subscription_requested = QtCore.Signal(int) - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None): - super().__init__(parent) - self.setMinimumSize(QtCore.QSize(10, 10)) - - # Clicking on/tabbing to widget restores focus - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setMouseTracking(True) - - # Set to True after initializeGL is called. Don't allow grabbing - # OpenGL context until that point. - self._gl_ready = False - - # Color management - self._ocio_tf = None - self._ocio_exposure = 0.0 - self._ocio_gamma = 1.0 - self._ocio_channel_hot = [1, 1, 1, 1] - self._ocio_proc_context = ProcessorContext() - self._ocio_proc = None - self._ocio_proc_cpu = None - self._ocio_proc_cache_id = None - self._ocio_shader_cache_id = None - self._ocio_shader_desc = None - self._ocio_tex_start_index = 1 # Start after image_tex - self._ocio_tex_ids = [] - self._ocio_uniform_ids = {} - - # MVP matrix components - self._model_view_mat = np.eye(4) - self._proj_mat = np.eye(4) - - # Keyboard shortcuts - self._shortcuts = [] - - # Mouse info - self._mouse_pressed = False - self._mouse_last_pos = QtCore.QPointF() - - # Image texture - self._image_array = None - self._image_tex = None - self._image_pos = np.array([0.0, 0.0]) - self._image_size = np.array([1.0, 1.0]) - self._image_scale = 1.0 - - # Image plane VAO - self._plane_vao = None - self._plane_position_vbo = None - self._plane_tex_coord_vbo = None - self._plane_index_vbo = None - - # GLSL shader program - self._vert_shader = None - self._frag_shader = None - self._shader_program = None - - # Setup keyboard shortcuts - self._install_shortcuts() - - def initializeGL(self) -> None: - """ - Set up OpenGL resources and state (called once). - """ - self._gl_ready = True - - self.makeCurrent() - - # Init image texture - self._image_tex = GL.glGenTextures(1) - GL.glActiveTexture(GL.GL_TEXTURE0) - GL.glBindTexture(GL.GL_TEXTURE_2D, self._image_tex) - GL.glTexImage2D( - GL.GL_TEXTURE_2D, - 0, - GL.GL_RGB32F, - self._image_size[0], - self._image_size[1], - 0, - GL.GL_RGB, - GL.GL_FLOAT, - 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 - ) - self._set_ocio_tex_params(GL.GL_TEXTURE_2D, ocio.INTERP_LINEAR) - - # Init image plane - # fmt: off - plane_position_data = np.array( - [ - -0.5, - 0.5, - 0.0, # top-left - 0.5, - 0.5, - 0.0, # top-right - 0.5, - -0.5, - 0.0, # bottom-right - -0.5, - -0.5, - 0.0, # bottom-left - ], - dtype=np.float32, - ) - # fmt: on - - plane_tex_coord_data = np.array( - [ - 0.0, - 1.0, # top-left - 1.0, - 1.0, # top-right - 1.0, - 0.0, # bottom-right - 0.0, - 0.0, # bottom-left - ], - dtype=np.float32, - ) - - plane_index_data = np.array( - [0, 1, 2, 0, 2, 3], # triangles: top-left, bottom-right - dtype=np.uint32, - ) - - self._plane_vao = GL.glGenVertexArrays(1) - GL.glBindVertexArray(self._plane_vao) - - ( - self._plane_position_vbo, - self._plane_tex_coord_vbo, - self._plane_index_vbo, - ) = GL.glGenBuffers(3) - - GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._plane_position_vbo) - GL.glBufferData( - GL.GL_ARRAY_BUFFER, - plane_position_data.nbytes, - plane_position_data, - GL.GL_STATIC_DRAW, - ) - 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) - GL.glBufferData( - GL.GL_ARRAY_BUFFER, - plane_tex_coord_data.nbytes, - 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.glEnableVertexAttribArray(1) - - GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._plane_index_vbo) - GL.glBufferData( - GL.GL_ELEMENT_ARRAY_BUFFER, - plane_index_data.nbytes, - plane_index_data, - GL.GL_STATIC_DRAW, - ) - - self._build_program() - - def resizeGL(self, w: int, h: int) -> None: - """ - Called whenever the widget is resized. - - :param w: Window width - :param h: Window height - """ - self.makeCurrent() - - GL.glViewport(0, 0, w, h) - - # Center image plane - # fmt: off - self._proj_mat = self._orthographic_proj_matrix( - -1.0, # Near - 1.0, # Far - -w / 2.0, # Left - w / 2.0, # Right - h / 2.0, # Top - -h / 2.0, # Bottom - ) - # fmt: on - - self._update_model_view_mat() - - def paintGL(self) -> None: - """ - Called whenever a repaint is needed. Calling ``update()`` will - schedule a repaint. - """ - self.makeCurrent() - - GL.glClearColor(0.0, 0.0, 0.0, 1.0) - GL.glClear(GL.GL_COLOR_BUFFER_BIT) - - if self._shader_program is not None: - GL.glUseProgram(self._shader_program) - - self._use_ocio_tex() - self._use_ocio_uniforms() - - # Set uniforms - mvp_mat = self._proj_mat @ self._model_view_mat - 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" - ) - GL.glUniform1i(image_tex_loc, 0) - - # Bind texture, VAO, and draw - GL.glActiveTexture(GL.GL_TEXTURE0 + 0) - GL.glBindTexture(GL.GL_TEXTURE_2D, self._image_tex) - - GL.glBindVertexArray(self._plane_vao) - - GL.glDrawElements( - GL.GL_TRIANGLES, 6, GL.GL_UNSIGNED_INT, ctypes.c_void_p(0) - ) - - GL.glBindVertexArray(0) - - def load_image(self, image_path: Path) -> None: - """ - Load an image into the image plane texture. - - :param image_path: Image file path - """ - config = ocio.GetCurrentConfig() - - # Get input color space (file rule) - color_space_name, rule_idx = config.getColorSpaceFromFilepath( - image_path.as_posix() - ) - if not color_space_name: - # Use previous or config default - if self._ocio_proc_context: - color_space_name = self._ocio_proc_context.input_color_space - else: - color_space_name = ocio.ROLE_DEFAULT - - if self._ocio_proc_context: - proc_context = ProcessorContext( - color_space_name, - self._ocio_proc_context.transform_item_type, - self._ocio_proc_context.transform_item_name, - self._ocio_proc_context.transform_direction, - ) - else: - proc_context = ProcessorContext(color_space_name) - - # Load image data via an available image library - self._image_array = load_image(image_path) - - width = self._image_array.shape[1] - height = self._image_array.shape[0] - - # Stash image size for pan/zoom calculations - self._image_pos = np.array([0, 1], dtype=np.float64) - self._image_size = np.array([width, height], dtype=np.float64) - - # Load image data into texture - self.makeCurrent() - - GL.glBindTexture(GL.GL_TEXTURE_2D, self._image_tex) - GL.glTexImage2D( - GL.GL_TEXTURE_2D, - 0, - GL.GL_RGB32F, - width, - height, - 0, - GL.GL_RGB, - GL.GL_FLOAT, - self._image_array.ravel(), - ) - - self.image_loaded.emit( - image_path, int(self._image_size[0]), int(self._image_size[1]) - ) - - self.update_ocio_proc(proc_context=proc_context) - self.fit() - - # Log image change after load and render - self.broadcast_image() - - def broadcast_image(self) -> None: - """ - Broadcast current image array, if one is loaded, through the - message queue for other app components. - """ - if self._image_array is not None: - message_queue.put_nowait(self._image_array) - - def input_color_space(self) -> str | None: - """ - :return: Current input OCIO color space name - """ - if self._ocio_proc_context: - return self._ocio_proc_context.input_color_space - else: - return None - - def transform(self) -> Optional[ocio.Transform]: - """ - :return: Current OCIO transform - """ - return self._ocio_tf - - def clear_transform(self) -> None: - """ - Clear current OCIO transform, passing through the input image. - """ - - self._ocio_tf = None - - self.update_ocio_proc( - ProcessorContext(self._ocio_proc_context.input_color_space), - force_update=True, - ) - - def reset_ocio_proc(self, update: bool = False) -> None: - """ - Reset the OCIO GPU renderer to a passthrough state. - - :param update: Whether to redraw viewport - """ - self._ocio_proc_context = None - self._ocio_tf = None - self._ocio_exposure = 0.0 - self._ocio_gamma = 1.0 - self._ocio_channel_hot = [1, 1, 1, 1] - - if update: - self.update_ocio_proc(force_update=True) - - def update_ocio_proc( - self, - proc_context: Optional[ProcessorContext] = None, - 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 - state. This will trigger a GL update IF the underlying OCIO ops - in the processor have changed. - - :param proc_context: Processor context data - :param transform: Optional main OCIO transform, to be applied - from the current config's scene reference space. - :param channel: ImagePlaneChannels value to toggle channel - isolation. - :param force_update: Set to True to update the viewport even - when the processor has not been updated. - """ - - # Update processor parameters - if proc_context is not None: - self._ocio_proc_context = proc_context - if transform is not None: - self._ocio_tf = transform - if channel is not None: - self._update_ocio_channel_hot(channel) - - config = ocio.GetCurrentConfig() - has_scene_linear = config.hasRole(ocio.ROLE_SCENE_LINEAR) - scene_ref_name = ( - ReferenceSpaceManager.scene_reference_space().getName() - ) - - # Build simplified viewing pipeline: - # - GPU: For viewport rendering - # - CPU: For pixel sampling, sans viewport adjustments - gpu_viewing_pipeline = ocio.GroupTransform() - cpu_viewing_pipeline = ocio.GroupTransform() - - # Convert to scene linear space if input space is known - 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, - ) - gpu_viewing_pipeline.appendTransform(to_scene_linear) - cpu_viewing_pipeline.appendTransform(to_scene_linear) - - # Dynamic exposure adjustment - gpu_viewing_pipeline.appendTransform( - ocio.ExposureContrastTransform( - exposure=self._ocio_exposure, dynamicExposure=True - ) - ) - - # 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 has_scene_linear: - to_scene_ref = ocio.ColorSpaceTransform( - src=ocio.ROLE_SCENE_LINEAR, dst=scene_ref_name - ) - gpu_viewing_pipeline.appendTransform(to_scene_ref) - cpu_viewing_pipeline.appendTransform(to_scene_ref) - else: - to_scene_ref = ocio.ColorSpaceTransform( - src=self._ocio_proc_context.input_color_space, - dst=scene_ref_name, - ) - gpu_viewing_pipeline.appendTransform(to_scene_ref) - cpu_viewing_pipeline.appendTransform(to_scene_ref) - - # Main transform - if self._ocio_tf is not None: - gpu_viewing_pipeline.appendTransform(self._ocio_tf) - 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 - ): - from_scene_ref = ocio.ColorSpaceTransform( - 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) - - # Channel view - gpu_viewing_pipeline.appendTransform( - ocio.MatrixTransform.View( - channelHot=self._ocio_channel_hot, - lumaCoef=config.getDefaultLumaCoefs(), - ) - ) - - # Dynamic gamma adjustment - gpu_viewing_pipeline.appendTransform( - ocio.ExposureContrastTransform( - gamma=self._ocio_gamma, pivot=1.0, dynamicGamma=True - ) - ) - - # Create GPU processor - 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 - cpu_proc = config.getProcessor( - cpu_viewing_pipeline, ocio.TRANSFORM_DIR_FORWARD - ) - self._ocio_proc = cpu_proc - self._ocio_proc_cpu = cpu_proc.getDefaultCPUProcessor() - - # Update GPU processor shaders and textures - self._ocio_shader_desc = ocio.GpuShaderDesc.CreateShaderDesc( - language=ocio.GPU_LANGUAGE_GLSL_4_0 - ) - self._ocio_proc_cache_id = gpu_proc.getCacheID() - ocio_gpu_proc = gpu_proc.getDefaultGPUProcessor() - ocio_gpu_proc.extractGpuShaderInfo(self._ocio_shader_desc) - - self._allocate_ocio_tex() - self._build_program() - - # Set initial dynamic property state - 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() - - # Log processor change after render - message_queue.put_nowait( - (self._ocio_proc_context, self._ocio_proc) - ) - - elif force_update: - self.update() - - # The transform and processor has not changed, but other app components - # which view it may have dropped 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) - ) - - def exposure(self) -> float: - """ - :return: Last set exposure dynamic property value - """ - return self._ocio_exposure - - def update_exposure(self, value: float) -> None: - """ - Update OCIO GPU renderer exposure. This is a dynamic property, - implemented as a GLSL uniform, so can be updated without - modifying the OCIO shader program or its dependencies. - - :param value: Exposure value in stops - """ - self._ocio_exposure = value - self._update_ocio_dyn_prop(ocio.DYNAMIC_PROPERTY_EXPOSURE, value) - self.update() - - def gamma(self) -> float: - """ - :return: Last set gamma dynamic property value - """ - return self._ocio_gamma - - def update_gamma(self, value: float) -> None: - """ - Update OCIO GPU renderer gamma. This is a dynamic property, - implemented as a GLSL uniform, so can be updated without - modifying the OCIO shader program or its dependencies. - - .. note:: - Value is floor clamped at 0.001 to prevent zero division - errors. - - :param value: Gamma value used like: pow(rgb, 1/gamma) - """ - # Translate gamma to exponent, enforcing floor - value = 1.0 / max(0.001, value) - - self._ocio_gamma = value - self._update_ocio_dyn_prop(ocio.DYNAMIC_PROPERTY_GAMMA, value) - self.update() - - def enterEvent(self, event: QtCore.QEvent) -> None: - for shortcut in self._shortcuts: - shortcut.setEnabled(True) - - def leaveEvent(self, event: QtCore.QEvent) -> None: - for shortcut in self._shortcuts: - shortcut.setEnabled(False) - - def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - self._mouse_pressed = True - self._mouse_last_pos = event.position() - - def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: - pos = event.position() - - if self._mouse_pressed: - offset = np.array([*(pos - self._mouse_last_pos).toTuple()]) - self._mouse_last_pos = pos - - self.pan(offset, update=True) - else: - widget_w = self.width() - widget_h = self.height() - - # Trace mouse position through the inverse MVP matrix to update sampled - # pixel. - screen_pos = np.array( - [ - pos.x() / widget_w * 2.0 - 1.0, - (widget_h - pos.y() - 1) / widget_h * 2.0 - 1.0, - 0.0, - 1.0, - ] - ) - model_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 - ) - - # Broadcast sample position - if ( - self._image_array is not None - and 0 <= pixel_pos[0] < self._image_size[0] - and 0 <= pixel_pos[1] < self._image_size[1] - ): - pixel_x = math.floor(pixel_pos[0]) - pixel_y = math.floor(pixel_pos[1]) - pixel_input = list(self._image_array[pixel_y, pixel_x]) - if len(pixel_input) < 3: - pixel_input += [0.0] * (3 - len(pixel_input)) - elif len(pixel_input) > 3: - pixel_input = pixel_input[:3] - - # Sample output pixel with CPU processor - if self._ocio_proc_cpu is not None: - pixel_output = self._ocio_proc_cpu.applyRGB(pixel_input) - else: - pixel_output = pixel_input.copy() - - 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) - - def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: - self._mouse_pressed = False - - def wheelEvent(self, event: QtGui.QWheelEvent) -> None: - w, h = self.width(), self.height() - - # Fit image to frame - if h > w: - min_scale = w / self._image_size[0] - else: - min_scale = h / self._image_size[1] - - # Fill frame with 1 pixel with 0.5 pixel overscan - max_scale = max(w, h) * 1.5 - - delta = event.angleDelta().y() / 360.0 * self._image_scale - scale = min(max_scale, max(min_scale, self._image_scale - delta)) - - self.zoom(event.position(), scale, update=True, absolute=True) - - def pan( - self, offset: np.ndarray, update: bool = True, absolute: bool = False - ) -> None: - """ - Pan the viewport by the specified offset in screen space. - - :param offset: Offset in pixels - :param update: Whether to redraw the viewport - :param absolute: When True, offset will be treated as an - absolute position to translate the viewport from its - origin. - """ - if self._image_scale > 0: - if absolute: - self._image_pos = offset / self._image_scale - else: - self._image_pos += offset / self._image_scale - - self._update_model_view_mat(update=update) - - def zoom( - self, - point: QtCore.QPoint, - amount: float, - update: bool = True, - absolute: bool = False, - ) -> None: - """ - Zoom the viewport by the specified scale offset amount. - - :param point: Viewport position to center zoom on - :param amount: Zoom scale amount - :param update: Whether to redraw the viewport - :param absolute: When True, amount will be treated as an - absolute scale to set the viewport to. - """ - offset = np.array([*(point - self.rect().center()).toTuple()]) - - self.pan(-offset, update=False) - - if absolute: - self._image_scale = amount - else: - self._image_scale += amount - - self._update_model_view_mat(update=False) - - self.pan(offset, update=update) - - if self._image_array is not None: - self.scale_changed.emit(self._image_scale) - - def fit(self, update: bool = True) -> None: - """ - Pan and zoom so the image fits within the viewport and is - centered. - - :param update: Whether to redraw the viewport - """ - w, h = self.width(), self.height() - - # Fit image to frame - if h > w: - scale = w / self._image_size[0] - else: - scale = h / self._image_size[1] - - self.zoom(QtCore.QPoint(), scale, update=False, absolute=True) - self.pan(np.array([0.0, 0.0]), update=update, absolute=True) - - def _install_shortcuts(self) -> None: - """ - Setup supported keyboard shortcuts. - """ - # R,G,B,A = view channel - # C = view color - for i, key in enumerate(("R", "G", "B", "A", "C")): - channel_shortcut = QtGui.QShortcut(QtGui.QKeySequence(key), self) - channel_shortcut.activated.connect( - partial(self.update_ocio_proc, channel=i) - ) - self._shortcuts.append(channel_shortcut) - - # Number keys = Subscribe to transform @ slot - for i in range(10): - subscribe_shortcut = QtGui.QShortcut( - QtGui.QKeySequence(str(i)), self - ) - subscribe_shortcut.activated.connect( - lambda slot=i: self.tf_subscription_requested.emit(slot) - ) - self._shortcuts.append(subscribe_shortcut) - - # 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.activated.connect( - lambda exponent=i: self.zoom( - self.rect().center(), float(2**exponent), absolute=True - ) - ) - self._shortcuts.append(scale_shortcut) - - # F = fit image to viewport - fit_shortcut = QtGui.QShortcut(QtGui.QKeySequence("F"), self) - fit_shortcut.activated.connect(self.fit) - self._shortcuts.append(fit_shortcut) - - def _compile_shader( - self, glsl_src: str, shader_type: GL.GLenum - ) -> Optional[GL.GLuint]: - """ - Compile GLSL shader and return its object ID. - - :param glsl_src: Shader source code - :param shader_type: Type of shader to be created, which is an - enum adhering to the formatting ``GL_*_SHADER``. - :return: Shader object ID, or None if shader compilation fails - """ - self.makeCurrent() - - shader = GL.glCreateShader(shader_type) - GL.glShaderSource(shader, glsl_src) - GL.glCompileShader(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) - ) - return None - - return shader - - def _build_program(self, force: bool = False) -> None: - """ - This builds the initial shader program, and rebuilds its - fragment shader whenever the OCIO GPU renderer changes. - - :param force: Whether to force a rebuild even if the OCIO - shader cache ID has not changed. - """ - if not self._gl_ready: - return - - self.makeCurrent() - - # If new shader cache ID matches previous cache ID, existing program - # can be reused. - shader_cache_id = self._ocio_shader_cache_id - if self._ocio_shader_desc and not force: - shader_cache_id = self._ocio_shader_desc.getCacheID() - if self._ocio_shader_cache_id == shader_cache_id: - return - - # Init shader program - if not self._shader_program: - self._shader_program = GL.glCreateProgram() - - # 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 - ) - if not self._vert_shader: - return - - GL.glAttachShader(self._shader_program, self._vert_shader) - - # Frag shader needs recompile each build (for OCIO changes) - if self._frag_shader: - GL.glDetachShader(self._shader_program, self._frag_shader) - GL.glDeleteShader(self._frag_shader) - - frag_src = GLSL_FRAG_SRC - if self._ocio_shader_desc: - # Inject OCIO shader block - 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 - ) - if not self._frag_shader: - return - - GL.glAttachShader(self._shader_program, self._frag_shader) - - # Link program - GL.glBindAttribLocation(self._shader_program, 0, "in_position") - GL.glBindAttribLocation(self._shader_program, 1, "in_texCoord") - - GL.glLinkProgram(self._shader_program) - 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) - ) - return - - # Store cache ID to detect reuse - self._ocio_shader_cache_id = shader_cache_id - - def _orthographic_proj_matrix( - self, - near: float, - far: float, - left: float, - right: float, - top: float, - bottom: float, - ) -> np.ndarray: - """ - Build orthographic projection matrix array from camera frustum - parameters. - """ - right_plus_left = right + left - right_minus_left = right - left - - top_plus_bottom = top + bottom - top_minus_bottom = top - bottom - - far_plus_near = far + near - far_minus_near = far - near - - tx = -right_plus_left / right_minus_left - ty = -top_plus_bottom / top_minus_bottom - tz = -far_plus_near / far_minus_near - - a = 2 / right_minus_left - 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]] - ) - - def _update_model_view_mat(self, update: bool = True) -> None: - """ - Re-calculate the model view matrix, which needs to be updated - prior to rendering if the image or window size have changed. - - :param bool update: Optionally redraw the window - """ - self._model_view_mat = np.eye(4) - - # 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[:2, -1] += [ - self._image_pos[0] * self._image_scale, - -self._image_pos[1] * self._image_scale, - ] - - self._model_view_mat *= self._image_size.tolist() + [1.0, 1.0] - - # Use nearest interpolation when scaling up to see pixels - if self._image_scale > 1.0: - self._set_ocio_tex_params(GL.GL_TEXTURE_2D, ocio.INTERP_NEAREST) - else: - self._set_ocio_tex_params(GL.GL_TEXTURE_2D, ocio.INTERP_LINEAR) - - if update: - self.update() - - def _set_ocio_tex_params( - self, tex_type: GL.GLenum, interpolation: ocio.Interpolation - ) -> None: - """ - Set texture parameters for an OCIO LUT texture based on its - type and interpolation. - - :param tex_type: OpenGL texture type (GL_TEXTURE_1/2/3D) - :param interpolation: Interpolation enum value - """ - 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 - ) - 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 - ) - - def _allocate_ocio_tex(self) -> None: - """ - Iterate and allocate 1/2/3D textures needed by the current - OCIO GPU processor. 3D LUTs become 3D textures and 1D LUTs - become 1D or 2D textures depending on their size. Since - textures have a hardware enforced width limitation, large LUTs - are wrapped onto multiple rows. - - .. note:: - Each time this runs, the previous set of textures are - deleted from GPU memory first. - """ - if not self._ocio_shader_desc: - return - - self.makeCurrent() - - # Delete previous textures - self._del_ocio_tex() - self._del_ocio_uniforms() - - tex_index = self._ocio_tex_start_index - - # Process 3D textures - for tex_info in self._ocio_shader_desc.get3DTextures(): - tex_data = tex_info.getValues() - - tex = GL.glGenTextures(1) - GL.glActiveTexture(GL.GL_TEXTURE0 + tex_index) - GL.glBindTexture(GL.GL_TEXTURE_3D, tex) - self._set_ocio_tex_params(GL.GL_TEXTURE_3D, tex_info.interpolation) - GL.glTexImage3D( - GL.GL_TEXTURE_3D, - 0, - GL.GL_RGB32F, - tex_info.edgeLen, - tex_info.edgeLen, - tex_info.edgeLen, - 0, - GL.GL_RGB, - GL.GL_FLOAT, - tex_data, - ) - - self._ocio_tex_ids.append( - ( - tex, - tex_info.textureName, - tex_info.samplerName, - GL.GL_TEXTURE_3D, - tex_index, - ) - ) - tex_index += 1 - - # Process 2D textures - for tex_info in self._ocio_shader_desc.getTextures(): - tex_data = tex_info.getValues() - - internal_fmt = GL.GL_RGB32F - fmt = GL.GL_RGB - if tex_info.channel == self._ocio_shader_desc.TEXTURE_RED_CHANNEL: - internal_fmt = GL.GL_R32F - fmt = GL.GL_RED - - tex = GL.glGenTextures(1) - GL.glActiveTexture(GL.GL_TEXTURE0 + tex_index) - - if tex_info.height > 1: - tex_type = GL.GL_TEXTURE_2D - GL.glBindTexture(tex_type, tex) - self._set_ocio_tex_params(tex_type, tex_info.interpolation) - GL.glTexImage2D( - tex_type, - 0, - internal_fmt, - tex_info.width, - tex_info.height, - 0, - fmt, - GL.GL_FLOAT, - tex_data, - ) - else: - tex_type = GL.GL_TEXTURE_1D - GL.glBindTexture(tex_type, tex) - self._set_ocio_tex_params(tex_type, tex_info.interpolation) - GL.glTexImage1D( - tex_type, - 0, - internal_fmt, - tex_info.width, - 0, - fmt, - GL.GL_FLOAT, - tex_data, - ) - - self._ocio_tex_ids.append( - ( - tex, - tex_info.textureName, - tex_info.samplerName, - tex_type, - tex_index, - ) - ) - tex_index += 1 - - def _del_ocio_tex(self) -> None: - """ - Delete all OCIO textures from the GPU. - """ - self.makeCurrent() - - for ( - tex, - tex_name, - sampler_name, - tex_type, - tex_index, - ) in self._ocio_tex_ids: - GL.glDeleteTextures([tex]) - del self._ocio_tex_ids[:] - - def _use_ocio_tex(self) -> None: - """ - Bind all OCIO textures to the shader program. - """ - self.makeCurrent() - - 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, - ) - - def _del_ocio_uniforms(self) -> None: - """ - Forget about the dynamic property uniforms needed for the - previous OCIO shader build. - """ - self._ocio_uniform_ids.clear() - - def _use_ocio_uniforms(self) -> None: - """ - Bind and/or update dynamic property uniforms needed for the - current OCIO shader build. - """ - if not self._ocio_shader_desc or not self._shader_program: - return - - self.makeCurrent() - - for name, uniform_data in self._ocio_shader_desc.getUniforms(): - if name not in self._ocio_uniform_ids: - uid = GL.glGetUniformLocation(self._shader_program, name) - self._ocio_uniform_ids[name] = uid - else: - uid = self._ocio_uniform_ids[name] - - if uniform_data.type == ocio.UNIFORM_DOUBLE: - GL.glUniform1f(uid, uniform_data.getDouble()) - - def _update_ocio_dyn_prop( - self, prop_type: ocio.DynamicPropertyType, value: Any - ) -> None: - """ - Update a specific OCIO dynamic property, which will be passed - to the shader program as a uniform. - - :param prop_type: Property type to update. Only one dynamic - property per type is supported per processor, so only the - first will be updated if there are multiple. - :param value: An appropriate value for the specific property - type. - """ - if not self._ocio_shader_desc: - return - - if self._ocio_shader_desc.hasDynamicProperty(prop_type): - dyn_prop = self._ocio_shader_desc.getDynamicProperty(prop_type) - dyn_prop.setDouble(value) - - def _update_ocio_channel_hot(self, channel: int) -> None: - """ - Update the OCIO GPU renderers channel view to either isolate a - specific channel or show them all. - - :param channel: ImagePlaneChannels value to toggle channel - isolation. - """ - # If index is in range, and we are viewing all channels, or a channel - # other than index, isolate channel at index. - if channel < 3 and ( - all(self._ocio_channel_hot) or not self._ocio_channel_hot[channel] - ): - for i in range(3): - self._ocio_channel_hot[i] = 1 if i == channel else 0 - - # Otherwise show all channels - else: - for i in range(3): - self._ocio_channel_hot[i] = 1 diff --git a/src/apps/ocioview/ocioview/viewer/image_viewer.py b/src/apps/ocioview/ocioview/viewer/image_viewer.py deleted file mode 100644 index 3a8be5eeb1..0000000000 --- a/src/apps/ocioview/ocioview/viewer/image_viewer.py +++ /dev/null @@ -1,955 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -from contextlib import contextmanager -from pathlib import Path -from typing import Generator, Optional, 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 ..utils import float_to_uint8, get_glyph_icon, SignalsBlocked -from ..widgets import ComboBox, CallbackComboBox, ColorSpaceComboBox -from .image_plane import ImagePlane - - -class ViewerChannels(object): - """ - Enum to describe all the toggleable channel view options in - ``ImagePlane``. - """ - - R, G, B, A, ALL = list(range(5)) - - -class ImageViewer(QtWidgets.QWidget): - """ - Image viewer widget, which can display an image with internal - 32-bit float precision. - """ - - FMT_GRAY_LABEL = f'{{v}}' - FMT_R_LABEL = f'{{v}}' - FMT_G_LABEL = f'{{v}}' - FMT_B_LABEL = f'{{v}}' - FMT_SWATCH_CSS = "background-color: rgb({r}, {g}, {b});" - FMT_IMAGE_SCALE = f'{{s:,d}}{FMT_GRAY_LABEL.format(v="%")}' - - PASSTHROUGH = "passthrough" - PASSTHROUGH_LABEL = FMT_GRAY_LABEL.format(v=f"{PASSTHROUGH}:") - - 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: - """Get viewer type icon.""" - return get_glyph_icon("mdi6.image-outline", size=ICON_SIZE_TAB) - - @classmethod - def viewer_type_label(cls) -> str: - """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 - - # Widgets - # --------------------------------------------------------------------- - - # Viewport - self.image_plane = ImagePlane(self) - self.image_plane.setSizePolicy( - QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding, - ) - ) - - # 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 = ColorSpaceComboBox(include_roles=True) - 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.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.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() - self.exposure_box.setToolTip(self.exposure_label.toolTip()) - self.exposure_box.setRange(-6.0, 6.0) - self.exposure_box.setValue(self.image_plane.exposure()) - - self.gamma_label = get_glyph_icon("mdi6.gamma", as_widget=True) - self.gamma_label.setToolTip("Gamma") - self.gamma_box = QtWidgets.QDoubleSpinBox() - self.gamma_box.setStepType(QtWidgets.QSpinBox.AdaptiveDecimalStepType) - self.gamma_box.setToolTip(self.gamma_label.toolTip()) - self.gamma_box.setRange(0.01, 4.0) - self.gamma_box.setValue(self.image_plane.gamma()) - - self.sample_precision_label = get_glyph_icon( - "mdi6.decimal-increase", as_widget=True - ) - self.sample_precision_label.setToolTip( - "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.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.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.image_h_value_label = QtWidgets.QLabel("0") - 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.image_y_value_label = QtWidgets.QLabel("0") - - self.input_sample_label = get_glyph_icon( - "mdi6.import", color=GRAY_COLOR, as_widget=True - ) - self.input_r_sample_label = QtWidgets.QLabel() - self.input_g_sample_label = QtWidgets.QLabel() - self.input_b_sample_label = QtWidgets.QLabel() - self.input_sample_swatch = QtWidgets.QLabel() - self.input_sample_swatch.setFixedSize(20, 20) - self.input_sample_swatch.setStyleSheet( - self.FMT_SWATCH_CSS.format(r=0, g=0, b=0) - ) - - self.output_sample_label = get_glyph_icon( - "mdi6.export", color=GRAY_COLOR, as_widget=True - ) - self.output_r_sample_label = QtWidgets.QLabel() - self.output_g_sample_label = QtWidgets.QLabel() - self.output_b_sample_label = QtWidgets.QLabel() - self.output_sample_swatch = QtWidgets.QLabel() - self.output_sample_swatch.setFixedSize(20, 20) - self.output_sample_swatch.setStyleSheet( - self.FMT_SWATCH_CSS.format(r=0, g=0, b=0) - ) - - # Layout - # --------------------------------------------------------------------- - - # 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("base_image_viewer__info_bar") - self.info_bar.setStyleSheet( - "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 - ) - - self.inspect_bar = QtWidgets.QFrame() - self.inspect_bar.setObjectName("base_image_viewer__status_bar") - self.inspect_bar.setStyleSheet( - "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(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.input_color_space_box.color_space_changed.connect( - self._on_input_color_space_changed - ) - 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 - # --------------------------------------------------------------------- - - 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) - - 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. - """ - 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=self._make_processor_context(), - transform=self._make_transform(), - force_update=force, - ) - - super().update() - - # Broadcast this viewer's image data for other app components - self.image_plane.broadcast_image() - - def reset(self) -> None: - """Reset viewer parameters without unloading the current image.""" - self.image_plane.reset_ocio_proc(update=False) - - # Update widgets to match image plane - 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() - - def load_image(self, image_path: Path) -> None: - """ - Load an image into the viewer. - - If no ``image_path`` is provided, a file dialog will allow the - user to choose one. - - :param image_path: Absolute path to image file - """ - self.image_plane.load_image(image_path) - - # Input color space could be changed by file rules on image - # load. Update the GUI without triggering a re-render. - 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 - of range index will restore the combined channel view. - - :param channel: ImageViewChannels channel view to toggle. - ALL always shows all channels. - """ - self.image_plane.update_ocio_proc(channel=channel) - - def exposure(self) -> float: - """Get 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: - """Get 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) - - 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]: - """Get current OCIO transform.""" - return self.image_plane.transform() - - def set_transform( - self, - slot: int, - transform_fwd: Optional[ocio.Transform], - transform_inv: Optional[ocio.Transform], - ) -> None: - """ - Update main OCIO transform for the viewing pipeline, to be - applied from the current config's scene reference space. - - :param slot: Transform subscription slot - :param transform_fwd: Forward transform - :param transform_inv: Inverse transform - """ - tf_direction = self.transform_direction() - - 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 - ) - ): - return - - self._tf_fwd = transform_fwd - self._tf_inv = transform_inv - - self.update() - - def clear_transform(self) -> None: - """ - Clear current OCIO transform, passing through the input image. - """ - self._tf_subscription_slot = -1 - self._tf_fwd = None - self._tf_inv = None - - if self.tf_box.currentIndex() != 0: - with SignalsBlocked(self.tf_box): - self.tf_box.setCurrentIndex(0) - - self.image_plane.clear_transform() - - 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: - """Get transform source config item name.""" - return self.tf_box.currentData(role=self.ROLE_ITEM_NAME) - - def transform_direction(self) -> ocio.TransformDirection: - """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: - """ - :param direction: Set the transform direction to be viewed - """ - self.tf_direction_button.setChecked( - direction == ocio.TRANSFORM_DIR_INVERSE - ) - - @contextmanager - def _ocio_signals_blocked(self) -> Generator: - """ - This context manager can be used to prevent automatic OCIO - processor updates while changing interconnected OCIO - parameters. - """ - 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 _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 ( - default_color_space is not None - and default_color_space in all_color_spaces - ): - return default_color_space - elif all_color_spaces: - return all_color_spaces[0] - else: - return "" - - def _get_displays(self) -> list[str]: - """Get all active OCIO displays.""" - config = ocio.GetCurrentConfig() - return list(config.getDisplays()) - - def _get_default_display(self) -> str: - """Get default OCIO display.""" - config = ocio.GetCurrentConfig() - return config.getDefaultDisplay() - - def _get_views(self) -> list[str]: - """ - Get all active OCIO views, given the current input color space. - """ - 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 _get_default_view(self) -> str: - """ - Get default OCIO view, given the current display and input - color space. - """ - config = ocio.GetCurrentConfig() - input_color_space = None - if input_color_space: - return config.getDefaultView(self.display(), input_color_space) - else: - return config.getDefaultView(self.display()) - - def _on_mode_changed(self) -> None: - """Called when the application mode changes.""" - mode = OCIOViewMode.current_mode() - - 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: - self.image_name_label.setText( - self.FMT_GRAY_LABEL.format(v=image_path.as_posix()) - ) - self.image_w_value_label.setText("0" if width == -1 else str(width)) - self.image_h_value_label.setText("0" if height == -1 else str(height)) - - @QtCore.Slot(float) - def _on_scale_changed(self, scale: float) -> None: - self.image_scale_label.setText( - self.FMT_IMAGE_SCALE.format(s=round(scale * 100)) - ) - - @QtCore.Slot(int, int, float, float, float, float, float, float) - def _on_sample_changed( - self, - x: int, - y: int, - r_input: float, - g_input: float, - b_input: float, - r_output: float, - g_output: float, - b_output: float, - ) -> None: - # Sample position - self.image_x_value_label.setText("0" if x == -1 else str(x)) - self.image_y_value_label.setText("0" if y == -1 else str(y)) - - # Input pixel sample - self.input_r_sample_label.setText( - self.FMT_R_LABEL.format(v=self._sample_format.format(v=r_input)) - ) - self.input_g_sample_label.setText( - self.FMT_G_LABEL.format(v=self._sample_format.format(v=g_input)) - ) - self.input_b_sample_label.setText( - self.FMT_B_LABEL.format(v=self._sample_format.format(v=b_input)) - ) - self.input_sample_swatch.setStyleSheet( - self.FMT_SWATCH_CSS.format( - r=float_to_uint8(r_input), - g=float_to_uint8(g_input), - b=float_to_uint8(b_input), - ) - ) - - # Output pixel sample - self.output_r_sample_label.setText( - self.FMT_R_LABEL.format(v=self._sample_format.format(v=r_output)) - ) - self.output_g_sample_label.setText( - self.FMT_G_LABEL.format(v=self._sample_format.format(v=g_output)) - ) - self.output_b_sample_label.setText( - self.FMT_B_LABEL.format(v=self._sample_format.format(v=b_output)) - ) - self.output_sample_swatch.setStyleSheet( - self.FMT_SWATCH_CSS.format( - r=float_to_uint8(r_output), - g=float_to_uint8(g_output), - b=float_to_uint8(b_output), - ) - ) - - def _on_input_color_space_changed(self) -> None: - self.image_plane.update_ocio_proc( - proc_context=self._make_processor_context() - ) - - @QtCore.Slot(float) - def _on_exposure_changed(self, value: float) -> None: - self.image_plane.update_exposure(value) - - @QtCore.Slot(float) - 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/offscreen_viewer.py b/src/apps/ocioview/ocioview/viewer/offscreen_viewer.py deleted file mode 100644 index ca785ab7a5..0000000000 --- a/src/apps/ocioview/ocioview/viewer/offscreen_viewer.py +++ /dev/null @@ -1,160 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -import numpy as np -import pygfx as gfx -from PySide6 import QtCore, QtGui, QtWidgets -from wgpu.gui.offscreen import WgpuCanvas -from wgpu.gui.qt import BUTTON_MAP, MODIFIERS_MAP - - -class WgpuCanvasOffScreenViewer(QtWidgets.QGraphicsView): - def __init__(self): - super().__init__() - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - - # WebGPU - self._wgpu_canvas = WgpuCanvas(size=self._viewport_size) - self._wgpu_renderer = gfx.renderers.WgpuRenderer(self._wgpu_canvas) - self._wgpu_camera = gfx.PerspectiveCamera(50, 16 / 9) - self._wgpu_controller = gfx.OrbitController(self._wgpu_camera) - self._wgpu_controller.register_events(self._wgpu_renderer) - - self._wgpu_scene = gfx.Scene() - - self._wgpu_canvas.request_draw( - lambda: self._wgpu_renderer.render( - self._wgpu_scene, self._wgpu_camera - ) - ) - - self._wgpu_camera.local.position = np.array([0.5, 0.5, 2]) - self._wgpu_camera.show_pos(np.array([0.5, 0.5, 0.5])) - - # QGraphicsView - self.setScene(QtWidgets.QGraphicsScene(self)) - self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse) - self.image_plane = QtWidgets.QGraphicsPixmapItem( - self._render_to_pixmap() - ) - self.scene().addItem(self.image_plane) - self.scale(0.5, 0.5) - - @property - def wgpu_canvas(self): - return self._wgpu_canvas - - @property - def wgpu_renderer(self): - return self._wgpu_renderer - - @property - def wgpu_camera(self): - return self._wgpu_camera - - @property - def wgpu_controller(self): - return self._wgpu_controller - - @property - def wgpu_scene(self): - return self._wgpu_scene - - @property - def _viewport_size(self): - return ( - self.viewport().size().width() * 2, - self.viewport().size().height() * 2, - ) - - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: - super().resizeEvent(event) - - self._wgpu_canvas.set_logical_size(*self._viewport_size) - - self.render() - - def _mouse_event(self, event_type, event, touches=True): - button = BUTTON_MAP.get(event.button(), 0) - buttons = [ - BUTTON_MAP[button] - for button in BUTTON_MAP.keys() - if button & event.buttons() - ] - - modifiers = [ - MODIFIERS_MAP[mod] - for mod in MODIFIERS_MAP.keys() - if mod & event.modifiers() - ] - - wgpu_event = { - "event_type": event_type, - "x": event.pos().x(), - "y": event.pos().y(), - "button": button, - "buttons": buttons, - "modifiers": modifiers, - } - if touches: - wgpu_event.update( - { - "ntouches": 0, - "touches": {}, - } - ) - - self._wgpu_canvas.handle_event(wgpu_event) - - self.render() - - def mousePressEvent(self, event): - self._mouse_event("pointer_down", event) - - def mouseMoveEvent(self, event): - self._mouse_event("pointer_move", event) - - def mouseReleaseEvent(self, event): - self._mouse_event("pointer_up", event) - - def mouseDoubleClickEvent(self, event): - self._mouse_event("double_click", event, touches=False) - - def wheelEvent(self, event): - modifiers = [ - MODIFIERS_MAP[mod] - for mod in MODIFIERS_MAP.keys() - if mod & event.modifiers() - ] - - wgpu_event = { - "event_type": "wheel", - "dx": -event.angleDelta().x(), - "dy": -event.angleDelta().y(), - "x": event.position().x(), - "y": event.position().y(), - "modifiers": modifiers, - } - - self._wgpu_canvas.handle_event(wgpu_event) - - self.render() - - def render(self): - self.image_plane.setPixmap(self._render_to_pixmap()) - - def _render_to_pixmap(self): - render = np.array(self._wgpu_renderer.target.draw())[..., :3] - - height, width, _channel = render.shape - return QtGui.QPixmap.fromImage( - QtGui.QImage( - np.ascontiguousarray(render), - width, - height, - 3 * width, - QtGui.QImage.Format_RGB888, - ) - ) diff --git a/src/apps/ocioview/ocioview/viewer/utils.py b/src/apps/ocioview/ocioview/viewer/utils.py deleted file mode 100644 index 5a2d4599d8..0000000000 --- a/src/apps/ocioview/ocioview/viewer/utils.py +++ /dev/null @@ -1,89 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from pathlib import Path - -import numpy as np - -try: - import OpenImageIO as oiio - - HAS_OIIO = True -except (ImportError, ModuleNotFoundError): - HAS_OIIO = False - oiio = None - -if not HAS_OIIO: - # NOTE: This will raise if not available, since one of the supported image - # libraries is required. - import imageio as iio -else: - iio = None - - -def load_image(image_path: Path) -> np.ndarray: - """ - Load RGB image data via an available image library. - - :param image_path: Path to image to load - :return: NumPy array - """ - if HAS_OIIO: - return _load_oiio(image_path) - else: - return _load_iio(image_path) - - -def _load_oiio(image_path: Path) -> np.ndarray: - """ - Load RGB image data via OpenImageIO. - - :param image_path: Path to image to load - :return: NumPy array - """ - image_buf = oiio.ImageBuf(image_path.as_posix()) - spec = image_buf.spec() - - # Convert to RGB, filling missing color channels with 0.0 - if spec.nchannels < 3: - image_buf = oiio.ImageBufAlgo.channels( - image_buf, - tuple(list(range(spec.nchannels)) + ([0.0] * (3 - spec.nchannels))), - newchannelnames=("R", "G", "B"), - ) - elif spec.nchannels > 3: - image_buf = oiio.ImageBufAlgo.channels( - image_buf, (0, 1, 2), newchannelnames=("R", "G", "B") - ) - - # Get pixels as 32-bit float NumPy array - return image_buf.get_pixels(oiio.FLOAT) - - -def _load_iio(image_path: Path) -> np.ndarray: - """ - Load RGB image data via imageio. - - :param image_path: Path to image to load - :return: NumPy array - """ - data = iio.imread(image_path.as_posix()) - - # Convert to 32-bit float - if not np.issubdtype(data.dtype, np.floating): - data = data.astype(np.float32) / np.iinfo(data.dtype).max - if data.dtype != np.float32: - data = data.astype(np.float32) - - # Convert to RGB, filling missing color channels with 0.0 - nchannels = 1 - if len(data.shape) == 3: - nchannels = data.shape[-1] - - while nchannels < 3: - data = np.dstack((data, np.zeros(data.shape[:2]))) - nchannels += 1 - if nchannels > 3: - data = data[..., :3] - - return data diff --git a/src/apps/ocioview/ocioview/viewer_dock.py b/src/apps/ocioview/ocioview/viewer_dock.py deleted file mode 100644 index 243e94363c..0000000000 --- a/src/apps/ocioview/ocioview/viewer_dock.py +++ /dev/null @@ -1,251 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from collections import defaultdict -from pathlib import Path -from typing import Optional - -from PySide6 import QtCore, QtWidgets - -from .settings import settings -from .transform_manager import TransformManager -from .utils import get_glyph_icon -from .viewer import ImageViewer -from .widgets.structure import TabbedDockWidget - - -class ViewerDock(TabbedDockWidget): - """ - Dockable widget for viewing color transforms on images. - """ - - SETTING_IMAGE_DIR = "image_dir" - SETTING_RECENT_IMAGES = "recent_images" - SETTING_RECENT_IMAGE_PATH = "path" - - 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"), - corner_widget=corner_widget, - parent=parent, - ) - - self._recent_images_menu = recent_images_menu - - self.setAllowedAreas(QtCore.Qt.NoDockWidgetArea) - self.tabs.setTabPosition(QtWidgets.QTabWidget.West) - self.tabs.currentChanged.connect(self._on_tab_changed) - - self._tab_bar = self.tabs.tabBar() - self._tab_bar.installEventFilter(self) - - # Widgets - self._viewers = defaultdict(list) - self.add_image_viewer() - - # Initialize - self._update_recent_images_menu() - - 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: - pos = event.pos() - tab_index = self._tab_bar.tabAt(pos) - tab_widget = self.tabs.widget(tab_index) - - tab_menu = QtWidgets.QMenu(self._tab_bar) - close_action = tab_menu.addAction( - "Close", lambda: self._on_tab_close_requested(tab_index) - ) - - if len(self._viewers.get(type(tab_widget), [])) == 1: - # Only enable the action if there is more than one viewer of this - # type open. - close_action.setEnabled(False) - - tab_menu.popup(self._tab_bar.mapToGlobal(pos)) - return True - - return False - - def load_image( - self, image_path: Optional[Path] = None, new_tab: bool = False - ) -> ImageViewer: - """ - Load an image into a new or existing viewer tab. - - :param image_path: Optional image path to load - :param new_tab: Whether to load image into a new tab instead of - the current or first available image viewer. - :return: Image viewer instance - """ - if new_tab or not self._viewers.get(ImageViewer): - image_viewer = self.add_image_viewer() - else: - current_viewer = self.tabs.currentWidget() - if isinstance(current_viewer, ImageViewer): - image_viewer = current_viewer - else: - image_viewer = self._viewers[ImageViewer][0] - - self.tabs.setCurrentWidget(image_viewer) - - if image_path is None or not image_path.is_file(): - image_dir = self._get_image_dir(image_path) - - # Prompt user to choose an image - image_path_str, sel_filter = QtWidgets.QFileDialog.getOpenFileName( - self, "Load image", dir=image_dir - ) - if not image_path_str: - return image_viewer - - image_path = Path(image_path_str) - - settings.setValue(self.SETTING_IMAGE_DIR, image_path.parent.as_posix()) - self._add_recent_image_path(image_path) - - image_viewer.load_image(image_path=image_path) - - return image_viewer - - def add_image_viewer(self) -> ImageViewer: - """ - Add a new image viewer tab to the dock. - - :return: Image viewer instance - """ - image_viewer = ImageViewer() - self._viewers[ImageViewer].append(image_viewer) - - self.add_tab( - image_viewer, - image_viewer.viewer_type_label(), - image_viewer.viewer_type_icon(), - ) - - return image_viewer - - def update_current_viewer(self) -> None: - """ - Update the current viewer to reflect the latest config changes. - """ - viewer = self.tabs.currentWidget() - viewer.update() - - def reset(self) -> None: - """ - Reset all viewer tabs to a passthrough state. - """ - for viewer_type, viewers in self._viewers.items(): - for viewer in viewers: - viewer.reset() - - TransformManager.reset() - - def _on_tab_changed(self, index: int) -> None: - """ - Track GL context with the current viewer. - """ - viewer = self.tabs.widget(index) - if viewer is not None: - # Force an update to trigger side effects of a processor change in the - # wider application. - viewer.update(force=True) - - def _on_tab_close_requested(self, index: int) -> None: - """ - Maintain one instance of each viewer type. - """ - viewer = self.tabs.widget(index) - viewer_type = type(viewer) - - if len(self._viewers.get(viewer_type, [])) > 1: - self.tabs.removeTab(index) - - if viewer in self._viewers[viewer_type]: - self._viewers[viewer_type].remove(viewer) - - def _get_image_dir(self, image_path: Optional[Path] = None) -> str: - """ - Infer an image load directory from an existing image path or - settings. - """ - image_dir = "" - if image_path is not None: - image_dir = image_path.parent.as_posix() - if not image_dir and settings.contains(self.SETTING_IMAGE_DIR): - image_dir = settings.value(self.SETTING_IMAGE_DIR) - return image_dir - - def _get_recent_image_paths(self) -> list[Path]: - """ - Get the 10 most recently loaded image file paths that still - exist. - - :return: List of image file paths - """ - recent_images = [] - - 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 - ) - if recent_image_path_str: - recent_image_path = Path(recent_image_path_str) - if recent_image_path.is_file(): - recent_images.append(recent_image_path) - settings.endArray() - - return recent_images - - def _add_recent_image_path(self, image_path: Path) -> None: - """ - Add the provided image file path to the top of the recent - image files list. - - :param image_path: Image file path - """ - image_paths = self._get_recent_image_paths() - if image_path in image_paths: - image_paths.remove(image_path) - image_paths.insert(0, image_path) - - if len(image_paths) > 10: - image_paths = image_paths[:10] - - settings.beginWriteArray(self.SETTING_RECENT_IMAGES) - for i, recent_image_path in enumerate(image_paths): - settings.setArrayIndex(i) - settings.setValue( - self.SETTING_RECENT_IMAGE_PATH, recent_image_path.as_posix() - ) - settings.endArray() - - # Update menu with latest list - self._update_recent_images_menu() - - def _update_recent_images_menu(self) -> None: - """Update recent image menu actions.""" - self._recent_images_menu.clear() - for recent_image_path in self._get_recent_image_paths(): - self._recent_images_menu.addAction( - recent_image_path.name, - lambda path=recent_image_path: self.load_image(path), - ) diff --git a/src/apps/ocioview/ocioview/widgets/__init__.py b/src/apps/ocioview/ocioview/widgets/__init__.py deleted file mode 100644 index 9f0bf2e0e4..0000000000 --- a/src/apps/ocioview/ocioview/widgets/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from .check_box import CheckBox -from .combo_box import ( - ComboBox, - EnumComboBox, - CallbackComboBox, - ColorSpaceComboBox, -) -from .layout import FormLayout -from .line_edit import ( - LineEdit, - PathEdit, - FloatEdit, - FloatEditArray, - IntEdit, - IntEditArray, -) -from .list_widget import StringListWidget, ItemModelListWidget -from .log_view import LogView -from .structure import TabbedDockWidget, ExpandingStackedWidget -from .table_widget import StringMapTableWidget, ItemModelTableWidget -from .text_edit import TextEdit, HtmlView diff --git a/src/apps/ocioview/ocioview/widgets/check_box.py b/src/apps/ocioview/ocioview/widgets/check_box.py deleted file mode 100644 index 7dcdbc828a..0000000000 --- a/src/apps/ocioview/ocioview/widgets/check_box.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from PySide6 import QtCore, QtWidgets - -from ..utils import SignalsBlocked - - -class CheckBox(QtWidgets.QCheckBox): - # DataWidgetMapper user property interface - @QtCore.Property(bool, user=True) - def __data(self) -> bool: - return self.value() - - @__data.setter - def __data(self, data: bool) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> bool: - return self.isChecked() - - def set_value(self, value: bool) -> None: - self.setChecked(value) - - def reset(self) -> None: - self.setChecked(False) diff --git a/src/apps/ocioview/ocioview/widgets/combo_box.py b/src/apps/ocioview/ocioview/widgets/combo_box.py deleted file mode 100644 index d8c63d29e7..0000000000 --- a/src/apps/ocioview/ocioview/widgets/combo_box.py +++ /dev/null @@ -1,451 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from __future__ import annotations - -import enum -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 - - -class ComboBox(QtWidgets.QComboBox): - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - self.setSizeAdjustPolicy( - QtWidgets.QComboBox.AdjustToMinimumContentsLengthWithIcon - ) - - # DataWidgetMapper user property interface - @QtCore.Property(str, user=True) - def __data(self) -> str: - return self.value() - - @__data.setter - def __data(self, data: str) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> str: - return self.currentText() - - def set_value(self, value: str) -> None: - self.setCurrentText(value) - - def reset(self) -> None: - if self.isEditable(): - self.setEditText("") - elif self.count(): - self.setCurrentIndex(0) - - -class EnumComboBox(ComboBox): - """Combo box with an enum model.""" - - def __init__( - self, - enum_type: enum.Enum, - icons: Optional[dict[enum.Enum, QtGui.QIcon]] = None, - parent: Optional[QtCore.QObject] = None, - ): - super().__init__(parent=parent) - - for name, member in enum_type.__members__.items(): - if icons is not None and member in icons: - self.addItem(icons[member], name, userData=member) - else: - self.addItem(name, userData=member) - - # DataWidgetMapper user property interface - @QtCore.Property(int, user=True) - def __data(self) -> int: - return self.currentIndex() - - @__data.setter - def __data(self, data: int) -> None: - with SignalsBlocked(self): - self.setCurrentIndex(data) - - # Direct enum member access - def member(self) -> enum.Enum: - return self.currentData() - - def set_member(self, value: enum.Enum) -> None: - self.setCurrentText(value.name) - - -class CallbackComboBox(ComboBox): - """Combo box modeled around provided item callback(s).""" - - def __init__( - self, - get_items: Callable, - get_default_item: Optional[Callable] = None, - item_icon: Optional[QtGui.QIcon] = None, - editable: bool = False, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param get_items: Required callback which receives no - parameters and returns a list of item strings, or a - dictionary with item string keys and QIcon values, to add - to combo box. - :param get_default_item: Optional callback which receives no - parameters and returns the default item string. If unset, - the first item is the default. - :param item_icon: Optionally provide one static icon for all - items. Icons provided by 'get_items' take precedence. - :param editable: Whether combo box is editable - """ - super().__init__(parent=parent) - - self._get_items = get_items - self._get_default_item = get_default_item - self._item_icon = item_icon - - self.setEditable(editable) - self.setInsertPolicy(QtWidgets.QComboBox.NoInsert) - - completer = self.completer() - if completer is not None: - completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) - - # Initialize - self.update_items() - - # DataWidgetMapper user property interface - @QtCore.Property(str, user=True) - def __data(self) -> str: - return self.value() - - @__data.setter - def __data(self, data: str) -> None: - with SignalsBlocked(self): - if self.findText(data) == -1: - self.update_items() - self.set_value(data) - - def update_items(self) -> str: - """ - Call the provided callback(s) to update combo box items. - - :return: Current item string - """ - # Get current state - current_item = self.currentText() - current_item_restored = False - - # Reload all items - with SignalsBlocked(self): - self.clear() - items = self._get_items() - if isinstance(items, dict): - for item, icon in items.items(): - if icon is None: - icon = self._item_icon - self.addItem(icon, item) - else: - if self._item_icon is not None: - for item in self._get_items(): - self.addItem(self._item_icon, item) - else: - self.addItems(self._get_items()) - - # 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() - - def showPopup(self) -> None: - """ - Reload items whenever the popup is shown for just-in-time - model updates. - """ - text = self.update_items() - - super().showPopup() - - # This selects the current item in the popup and must be called after the - # popup is shown. - items = self.model().findItems(text) - if items: - self.view().setCurrentIndex(items[0].index()) - - 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/item_view.py b/src/apps/ocioview/ocioview/widgets/item_view.py deleted file mode 100644 index 44d05fe186..0000000000 --- a/src/apps/ocioview/ocioview/widgets/item_view.py +++ /dev/null @@ -1,246 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Callable, Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_BUTTON, ICON_SIZE_ITEM -from ..style import apply_top_tool_bar_style, apply_widget_with_top_tool_bar_style -from ..utils import get_glyph_icon -from .line_edit import LineEdit - - -class BaseItemView(QtWidgets.QFrame): - """ - Abstract base class for adding a filter edit, add, remove, move up, - and move down buttons to an item view. - """ - - items_changed = QtCore.Signal() - current_row_changed = QtCore.Signal(int) - - DEFAULT_ITEM_FLAGS = ( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable - ) - - def __init__( - self, - item_view: QtWidgets.QListView, - item_flags: QtCore.Qt.ItemFlags = DEFAULT_ITEM_FLAGS, - item_icon: Optional[QtGui.QIcon] = None, - items_constant: bool = False, - items_movable: bool = True, - get_presets: Optional[Callable] = None, - presets_only: bool = False, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param item_view: Item view to wrap - :param item_flags: list item flags - :param item_icon: Optional item icon - :param items_constant: Optionally hide the add and remove - buttons, for implementations where items are - auto-populated. Note that preset support is dependent on - this being False. - :param items_movable: Optionally hide item movement buttons, - for implementations where items are auto-sorted. - :param get_presets: Optional callback which returns either a - list of string presets, or a dictionary of string presets - and corresponding item icons, that can be selected from an - add button popup menu. - :param presets_only: When True, only preset items may be added. - Clicking the add button will present the preset menu - instead of adding an item to the view. - """ - super().__init__(parent=parent) - - self._item_flags = item_flags - self._item_icon = item_icon - self._items_constant = items_constant - self._items_movable = items_movable - self._has_presets = get_presets is not None - self._get_presets = get_presets or (lambda: []) - self._presets_only = presets_only - - self.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.setObjectName("item_view") - apply_widget_with_top_tool_bar_style(self) - - # Widgets - self.filter_edit = LineEdit() - self.filter_edit.setClearButtonEnabled(True) - self.filter_edit.setPlaceholderText("filter") - self.filter_edit.textChanged.connect(self._on_filter_text_changed) - - if not self._items_constant: - # Add button preset menu - self.preset_menu = QtWidgets.QMenu(self) - self.preset_menu.aboutToShow.connect(self._on_preset_menu_requested) - self.preset_menu.triggered.connect(self._on_preset_triggered) - - self.add_button = QtWidgets.QToolButton(self) - self.add_button.setIconSize(ICON_SIZE_BUTTON) - self.add_button.setIcon(get_glyph_icon("ph.plus")) - self.add_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.add_button.released.connect(self._on_add_button_released) - if self._has_presets: - self.add_button.setMenu(self.preset_menu) - if self._presets_only: - self.add_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.add_button.setToolButtonStyle( - QtCore.Qt.ToolButtonTextBesideIcon - ) - else: - self.add_button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) - - self.remove_button = QtWidgets.QToolButton(self) - self.remove_button.setIconSize(ICON_SIZE_BUTTON) - self.remove_button.setIcon(get_glyph_icon("ph.minus")) - self.remove_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.remove_button.released.connect(self._on_remove_button_released) - - if self._items_movable: - self.move_up_button = QtWidgets.QToolButton(self) - self.move_up_button.setIconSize(ICON_SIZE_BUTTON) - self.move_up_button.setIcon(get_glyph_icon("ph.arrow-up")) - self.move_up_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.move_up_button.released.connect(self._on_move_up_button_released) - - self.move_down_button = QtWidgets.QToolButton(self) - self.move_down_button.setIconSize(ICON_SIZE_BUTTON) - 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(self._on_move_down_button_released) - - self.view = item_view - self.view.setIconSize(ICON_SIZE_ITEM) - - # Layout - tool_bar = QtWidgets.QToolBar() - tool_bar.setStyleSheet( - "QToolButton::menu-indicator {" - " subcontrol-position: center right;" - " right: 4px;" - "}" - ) - tool_bar.setContentsMargins(0, 0, 0, 0) - tool_bar.setIconSize(ICON_SIZE_ITEM) - tool_bar.addWidget(self.filter_edit) - tool_bar.addWidget(QtWidgets.QLabel("")) - if not self._items_constant: - tool_bar.addWidget(self.add_button) - tool_bar.addWidget(self.remove_button) - if self._items_movable: - tool_bar.addWidget(self.move_up_button) - tool_bar.addWidget(self.move_down_button) - - tool_bar_layout = QtWidgets.QVBoxLayout() - tool_bar_layout.setContentsMargins(0, 0, 0, 0) - tool_bar_layout.addWidget(tool_bar) - - tool_bar_frame = QtWidgets.QFrame() - tool_bar_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - tool_bar_frame.setObjectName("item_view__tool_bar_frame") - apply_top_tool_bar_style(tool_bar_frame) - tool_bar_frame.setLayout(tool_bar_layout) - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(1) - layout.addWidget(tool_bar_frame) - layout.addWidget(self.view) - - self.setLayout(layout) - - def reset(self) -> None: - self.clear() - - def clear(self) -> None: - """Remove all items from list.""" - raise NotImplementedError - - def items(self) -> list[str]: - """ - :return: list of item names - """ - raise NotImplementedError - - def set_current_item(self, text: str) -> tuple[bool, int]: - """ - :param text: Make the named item the current item - :return: Whether the requested item was selected, and the - current row after any changes - """ - raise NotImplementedError - - def add_item(self, text: Optional[str] = None) -> None: - """ - Create a new list item. - - :param text: Optional item name - """ - raise NotImplementedError - - def remove_item(self, text: str) -> None: - """ - :param text: Name of list item to remove - """ - raise NotImplementedError - - @QtCore.Slot(str) - def _on_filter_text_changed(self, text: str) -> None: - """ - Subclasses must implement list filtering behavior, hiding items - which don't contain the provided search term. - - :param text: Filter search term - """ - raise NotImplementedError - - def _on_add_button_released(self) -> None: - """ - Subclasses must implement behavior which results from the - widget's add button being clicked. - """ - raise NotImplementedError - - def _on_remove_button_released(self) -> None: - """ - Subclasses must implement behavior which results from the - widget's remove button being clicked. - """ - raise NotImplementedError - - def _on_move_up_button_released(self) -> None: - """ - Subclasses must implement behavior which results from the - widget's move up button being clicked. - """ - raise NotImplementedError - - def _on_move_down_button_released(self) -> None: - """ - Subclasses must implement behavior which results from the - widget's move down button being clicked. - """ - raise NotImplementedError - - def _on_preset_menu_requested(self) -> None: - """Repopulate preset menu from callback.""" - self.preset_menu.clear() - - presets = self._get_presets() - if isinstance(presets, dict): - for preset, item_icon in presets.items(): - self.preset_menu.addAction(item_icon, preset) - else: - for preset in presets: - if self._item_icon: - self.preset_menu.addAction(self._item_icon, preset) - else: - self.preset_menu.addAction(preset) - - def _on_preset_triggered(self, action: QtGui.QAction) -> None: - """Add a new item from the triggered preset.""" - self.add_item(action.text()) diff --git a/src/apps/ocioview/ocioview/widgets/layout.py b/src/apps/ocioview/ocioview/widgets/layout.py deleted file mode 100644 index 331b057f26..0000000000 --- a/src/apps/ocioview/ocioview/widgets/layout.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtWidgets - - -class FormLayout(QtWidgets.QFormLayout): - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - self.setLabelAlignment(QtCore.Qt.AlignRight) diff --git a/src/apps/ocioview/ocioview/widgets/line_edit.py b/src/apps/ocioview/ocioview/widgets/line_edit.py deleted file mode 100644 index 0562cb5492..0000000000 --- a/src/apps/ocioview/ocioview/widgets/line_edit.py +++ /dev/null @@ -1,509 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from contextlib import contextmanager -from pathlib import Path -from typing import Any, Optional, Sequence - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import R_COLOR, G_COLOR, B_COLOR -from ..utils import SignalsBlocked, get_glyph_icon - - -class LineEdit(QtWidgets.QLineEdit): - def __init__( - self, text: Optional[str] = None, parent: Optional[QtCore.QObject] = None - ): - super().__init__(parent=parent) - - if text is not None: - self.setText(str(text)) - - # DataWidgetMapper user property interface - @QtCore.Property(str, user=True) - def __data(self) -> str: - return self.value() - - @__data.setter - def __data(self, data: str) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> str: - return self.text() - - def set_value(self, value: str) -> None: - self.setText(str(value)) - - def reset(self) -> None: - self.clear() - - -class PathEdit(LineEdit): - """ - File or directory path line edit with browse button. - """ - - BROWSE_GLYPHS = { - QtWidgets.QFileDialog.AnyFile: "ph.file", - QtWidgets.QFileDialog.ExistingFile: "ph.file", - QtWidgets.QFileDialog.Directory: "ph.folder", - QtWidgets.QFileDialog.ShowDirsOnly: "ph.folder", - } - - def __init__( - self, - file_mode: QtWidgets.QFileDialog.FileMode, - path: Optional[Path] = None, - browse_filter: str = "", - parent: Optional[QtCore.QObject] = None, - ): - """ - :param file_mode: Defines the type of filesystem item to browse - for. - :param path: Optional initial path - :param browse_filter: Optional file browser filter (see - QFileBrowser documentation for details). - """ - super().__init__(parent=parent) - - self._file_mode = file_mode - self._filter = browse_filter - - if self._file_mode in self.BROWSE_GLYPHS: - self._browse_action = self.addAction( - get_glyph_icon(self.BROWSE_GLYPHS[self._file_mode]), - self.ActionPosition.TrailingPosition, - ) - self._browse_action.triggered.connect(self._on_browse_action_triggered) - - if path is not None: - self.set_path(path) - - # DataWidgetMapper user property interface - @QtCore.Property(Path, user=True) - def __data(self) -> Path: - return self.path() - - @__data.setter - def __data(self, data: Path) -> None: - with SignalsBlocked(self): - self.set_path(data) - - # Direct path access - def path(self) -> Path: - return Path(self.text()) - - def set_path(self, path: Path) -> None: - self.setText(path.as_posix()) - - def _on_browse_action_triggered(self) -> None: - """ - Browse file system for file or directory. - """ - # Get directory path from current text - path = self.path() - if path.is_file() or path.suffix: - path = path.parent - dir_str = path.as_posix() - - # Configure browser - kwargs = {"dir": dir_str} - if self._file_mode in ( - QtWidgets.QFileDialog.AnyFile, - QtWidgets.QFileDialog.ExistingFile, - ): - kwargs["filter"] = self._filter - elif self._file_mode == QtWidgets.QFileDialog.DirectoryOnly: - kwargs["options"] = QtWidgets.QFileDialog.ShowDirsOnly - - # Browse... - if self._file_mode == QtWidgets.QFileDialog.AnyFile: - path_str = QtWidgets.QFileDialog.getSaveFileName( - self, caption="Save As File", **kwargs - ) - elif self._file_mode == QtWidgets.QFileDialog.ExistingFile: - path_str = QtWidgets.QFileDialog.getOpenFileName( - self, caption="Choose File", **kwargs - ) - else: - path_str = QtWidgets.QFileDialog.getExistingDirectory( - self, caption="Choose Directory", **kwargs - ) - - # Push path back to line edit - if path_str: - self.set_path(Path(path_str)) - - -class BaseValueEdit(LineEdit): - """ - Base line edit for numeric string entry. - - NOTE: This widget implements its own builtin validation rather than - using a QValidator. This choice was made to improve handling - of text selection on return pressed in cooperation with being - driven by a DataWidgetMapper. - """ - - __value_type__ = None - """Numeric data type to convert input to.""" - - value_changed = QtCore.Signal(__value_type__) - - def __init__( - self, - default: Optional[__value_type__] = None, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param default: Default numeric value - """ - super().__init__(parent=parent) - self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - - # Store the last valid state, which will be restored when invalid input is - # received. - self._last_valid_value = None - - if default is None: - default = self.__value_type__() - else: - default = self.__value_type__(default) - self.default = default - self.set_value(self.default) - - # Connections - self.editingFinished.connect(self._on_editing_finished) - self.returnPressed.connect(self._on_return_pressed) - - # Common public interface - def value(self) -> Any: - return self.__value_type__(self.text()) - - def set_value(self, value: Any, raise_exc: bool = False) -> None: - """ - :param value: Value to set - :param raise_exc: Whether to raise an exception if value is - invalid, instead of falling back to the last valid or - default value. - """ - with self._preserve_select_all(): - self.setText(str(value)) - - self.validate(raise_exc=raise_exc) - - def reset(self) -> None: - """Restore default value.""" - self.set_value(self.default) - self.validate() - - def format(self, value: __value_type__) -> str: - """ - Subclasses must implement numeric value to string conversion in - this method. - - :param value: Numeric value - :return: Formatted value string - """ - raise NotImplementedError - - def validate(self, raise_exc: bool = False) -> None: - """ - Validate stored text as a representation of the expected - numeric type and reformat as needed per the implemented - ``format`` method. - - :param raise_exc: Whether to raise an exception if value is - invalid, instead of falling back to the last valid or - default value. - """ - try: - # Try to convert the string to the configured numeric type - value = self.__value_type__(self.text()) - except (ValueError, TypeError): - if raise_exc: - raise - - # Value is invalid. Reset to the last valid or default value, which are - # guaranteed to be valid. Allow an exception to be raised on the next - # validate cycle in the off chance the default value is itself invalid. - if self._last_valid_value is not None: - self.set_value(self._last_valid_value, raise_exc=True) - else: - self.set_value(self.default, raise_exc=True) - return - - # Value is valid - self._last_valid_value = value - - # Format new value text - with self._preserve_select_all(): - self.setText(self.format(value)) - - def _on_editing_finished(self) -> None: - self.validate() - self.value_changed.emit(self.value()) - - def _on_return_pressed(self) -> None: - # Select all when the user indicates they are done entering a value. This makes - # it easy to start entering a new value when iterating on a parameter. - self.selectAll() - - @contextmanager - def _preserve_select_all(self): - """ - Context manager which preserves select-all state through value - changes. This prevents dropping this state when reformatting - values or receiving model updates. - """ - select_all = False - if self.hasSelectedText() and self.selectionLength() == len(self.text()): - select_all = True - - yield - - if select_all: - self.selectAll() - - -class FloatEdit(BaseValueEdit): - __value_type__ = float - - value_changed = QtCore.Signal(__value_type__) - - def __init__( - self, - default: Optional[__value_type__] = None, - int_reduction: bool = True, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param default: Default numeric value - :param int_reduction: When set to True (the default), whole - number floats are formatted as integers. - """ - self._int_reduction = int_reduction - - super().__init__(default, parent) - - # DataWidgetMapper user property interface - @QtCore.Property(float, user=True) - def __data(self) -> float: - return self.value() - - @__data.setter - def __data(self, data: float) -> None: - with SignalsBlocked(self): - self.set_value(data) - - def format(self, value: float) -> str: - # 1.000 -> 1. - formatted = f"{float(value):.15f}".rstrip("0") - if self._int_reduction: - # 1. -> 1 - formatted = formatted.rstrip(".") - elif formatted.endswith("."): - # 1. -> 1.0 - formatted += "0" - return formatted - - -class IntEdit(BaseValueEdit): - __value_type__ = int - - value_changed = QtCore.Signal(__value_type__) - - # DataWidgetMapper user property interface - @QtCore.Property(int, user=True) - def __data(self) -> int: - return self.value() - - @__data.setter - def __data(self, data: int) -> None: - with SignalsBlocked(self): - self.set_value(data) - - def format(self, value: float) -> str: - return str(int(value)) - - -class BaseValueEditArray(QtWidgets.QWidget): - """Base widget for numeric string array entry.""" - - __value_type__: type = None - """Numeric data type to convert input to.""" - - __value_edit_type__: BaseValueEdit = None - """Value edit widget type to use per array component.""" - - value_changed = QtCore.Signal(str, __value_type__) - - LABEL_COLORS = {"r": R_COLOR, "g": G_COLOR, "b": B_COLOR} - - def __init__( - self, - labels: Sequence[str], - defaults: Optional[Sequence[Any]] = None, - shape: Optional[tuple[int, int]] = None, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param labels: Label per array component in row major order - :param defaults: Default values, matching label order - :param shape: Array shape as (columns, rows). If unset, shape - will default to (label count, 1). - """ - super().__init__(parent=parent) - - # Labels - self.labels = labels - num_labels = len(self.labels) - has_labels = bool(list(filter(None, self.labels))) - - # Default values - if defaults is None: - defaults = [self.__value_type__()] * num_labels - else: - num_defaults = len(defaults) - defaults = [self.__value_type__(v) for v in defaults] - if num_defaults < num_labels: - defaults += [self.__value_type__()] * (num_labels - num_defaults) - self.defaults = defaults - - # 2D array shape - if shape is None: - shape = (num_labels, 1) - self.shape = shape - columns, rows = self.shape - - # Build array widgets and layout - self.value_edits = [] - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - - for row in range(rows): - if len(self.value_edits) == num_labels: - break - - row_layout = QtWidgets.QHBoxLayout() - row_layout.setContentsMargins(0, 0, 0, 0) - if not has_labels: - row_layout.setSpacing(0) - - for column in range(columns): - if len(self.value_edits) == num_labels: - break - - index = row * columns + column - label = self.labels[index] - value_label = QtWidgets.QLabel(label) - if label in self.LABEL_COLORS: - value_label.setStyleSheet( - f"color: {self.LABEL_COLORS[label].name()};" - ) - - value_edit = self.__value_edit_type__(default=defaults[index]) - value_edit.value_changed.connect( - lambda v, l=label: self.value_changed.emit(l, v) - ) - self.value_edits.append(value_edit) - - row_layout.addWidget(value_label) - row_layout.setStretchFactor(value_label, 0) - row_layout.addWidget(value_edit) - row_layout.setStretchFactor(value_edit, 1) - - layout.addLayout(row_layout) - - self.setLayout(layout) - - # DataWidgetMapper user property interface - @QtCore.Property(list, user=True) - def __data(self) -> list[__value_type__]: - return self.value() - - @__data.setter - def __data(self, data: list[__value_type__]) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> list[__value_type__]: - """ - :return: list of all array values in row major order - """ - return [s.value() for s in self.value_edits] - - def set_value(self, values: Sequence[__value_type__]) -> None: - """ - :param values: Sequence of array values in row major order - """ - with SignalsBlocked(self, *self.value_edits): - for i, value in enumerate(values): - if i < len(self.value_edits): - self.value_edits[i].set_value(value) - - def component_value(self, label: str) -> __value_type__: - """ - :param label: Label of component to get - :return: Value for one array component. If label is invalid, - the value type default constructor value is returned - (usually equivalent to 0). - """ - value_edit = self._get_value_edit(label) - if value_edit is not None: - return value_edit.value() - return self.__value_type__() - - def set_component_value(self, label: str, value: __value_type__) -> None: - """ - :param label: Label for array component to set - :param value: Value to set - """ - value_edit = self._get_value_edit(label) - if value_edit is not None: - value_edit.set_value(value) - - def reset(self, label: Optional[str] = None) -> None: - """ - Restore default value. - - :param label: Optional label of component to reset. If unset, - all values will be reset. - """ - with SignalsBlocked(self, *self.value_edits): - if label is None: - for value_edit in self.value_edits: - value_edit.reset() - else: - value_edit = self._get_value_edit(label) - if value_edit is not None: - value_edit.reset() - - def _get_value_edit(self, label: str) -> Optional[BaseValueEdit]: - """ - :param label: Label of array component to get widget for - :return: Value edit widget, or None if no component with label - was found. - """ - if label in self.labels: - return self.value_edits[self.labels.index(label)] - return None - - -class FloatEditArray(BaseValueEditArray): - __value_type__ = float - __value_edit_type__ = FloatEdit - - value_changed = QtCore.Signal(str, __value_type__) - - -class IntEditArray(BaseValueEditArray): - __value_type__ = int - __value_edit_type__ = IntEdit - - value_changed = QtCore.Signal(str, __value_type__) diff --git a/src/apps/ocioview/ocioview/widgets/list_widget.py b/src/apps/ocioview/ocioview/widgets/list_widget.py deleted file mode 100644 index bec4eb682e..0000000000 --- a/src/apps/ocioview/ocioview/widgets/list_widget.py +++ /dev/null @@ -1,402 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Callable, Optional, TYPE_CHECKING, Union - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..utils import SignalsBlocked, next_name -from .item_view import BaseItemView - -if TYPE_CHECKING: - from ..items.config_item_model import BaseConfigItemModel - - -class StringListWidget(BaseItemView): - """ - Simple string list widget with filter edit and add and remove - buttons. - """ - - def __init__( - self, - item_basename: Optional[str] = None, - item_flags: QtCore.Qt.ItemFlags = BaseItemView.DEFAULT_ITEM_FLAGS, - item_icon: Optional[QtGui.QIcon] = None, - allow_empty: bool = True, - get_presets: Optional[Callable] = None, - presets_only: bool = False, - get_item: Optional[Callable] = None, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param item_basename: Optional basename for prefixing new item - names, formatted like: "_". The number - suffix is incremented so that all names are unique. - :param item_flags: list item flags - :param item_icon: Optional item icon - :param allow_empty: If set to False, the remove button will do - nothing when there is only one item. - :param get_presets: Optional callback which returns either a - list of string presets, or a dictionary of string presets - and corresponding item icons, that can be selected from an - add button popup menu. - :param presets_only: When True, only preset items may be added. - Clicking the add button will present the preset menu - instead of adding an item to the view. - :param get_item: Optional callback to request one new item from - the user (e.g. via a dialog). The callback should return a - string or ``None``, to indicate that no item should be - added. - """ - list_view = QtWidgets.QListWidget() - list_view.itemChanged.connect(lambda i: self.items_changed.emit()) - list_view.currentRowChanged.connect(self.current_row_changed.emit) - - super().__init__( - list_view, - item_flags=item_flags, - item_icon=item_icon, - get_presets=get_presets, - presets_only=presets_only, - parent=parent, - ) - - self._item_basename = item_basename or "" - self._allow_empty = allow_empty - self._get_item = get_item - - # DataWidgetMapper user property interface - @QtCore.Property(list, user=True) - def __data(self) -> list[str]: - return self.items() - - @__data.setter - def __data(self, data: list[str]) -> None: - with SignalsBlocked(self, self.view): - self.set_items(data) - - def clear(self) -> None: - self.view.clear() - - def items(self) -> list[str]: - return [self.view.item(row).text() for row in range(self.view.count())] - - def set_current_item(self, text: str) -> tuple[bool, int]: - items = self.view.findItems(text, QtCore.Qt.MatchExactly) - if items: - self.view.setCurrentItem(items[0]) - return True, self.view.currentRow() - return False, self.view.currentRow() - - def add_item(self, text: Optional[str] = None) -> None: - text = text or "" - if self.view.findItems(text, QtCore.Qt.MatchExactly): - # Item already exists - return - - if self._item_icon is not None: - item = QtWidgets.QListWidgetItem(self._item_icon, text) - else: - item = QtWidgets.QListWidgetItem(text) - - item.setFlags(self._item_flags) - self.view.addItem(item) - - self.view.setCurrentItem(item) - self.items_changed.emit() - - def remove_item(self, text_or_item: Union[str, QtWidgets.QListWidgetItem]) -> None: - if isinstance(text_or_item, QtWidgets.QListWidgetItem): - self.view.takeItem(self.view.row(text_or_item)) - else: - for item in sorted( - self.view.findItems(str(text_or_item), QtCore.Qt.MatchExactly), - key=lambda i: self.view.row(i), - reverse=True, - ): - self.view.takeItem(self.view.row(item)) - - self.items_changed.emit() - - def set_items(self, items: list[str]) -> None: - """ - Replace all items with the provided strings. - - :param items: list of item names - """ - self.view.clear() - - if items: - self.view.addItems(items) - for row in range(self.view.count()): - item = self.view.item(row) - item.setFlags(self._item_flags) - if self._item_icon is not None: - item.setIcon(self._item_icon) - - self.items_changed.emit() - - @QtCore.Slot(str) - def _on_filter_text_changed(self, text: str) -> None: - if len(text) < 2: - for row in range(self.view.count()): - self.view.setRowHidden(row, False) - return - - for row in range(self.view.count()): - self.view.setRowHidden(row, True) - - for item in self.view.findItems(text, QtCore.Qt.MatchContains): - self.view.setRowHidden(self.view.row(item), False) - - def _on_add_button_released(self) -> None: - if self._get_item is not None: - # Use provided callback to get next name - name = self._get_item() - if name is not None: - self.add_item(name) - return - - # Generate next name from provided basename, or fallback to an empty item - if self._item_basename: - self.add_item(next_name(f"{self._item_basename}_", self.items())) - else: - self.add_item("") - - def _on_remove_button_released(self) -> None: - for item in sorted( - self.view.selectedItems(), key=lambda i: self.view.row(i), reverse=True - ): - if not self._allow_empty and self.view.count() == 1: - QtWidgets.QMessageBox.warning( - self, - "Warning", - f"At least one {self._item_basename or 'item'} is required.", - ) - continue - self.remove_item(item) - - def _on_move_up_button_released(self) -> None: - if self.view.selectedItems(): - src_row = self.view.currentRow() - dst_row = max(0, src_row - 1) - self._move_item(src_row, dst_row) - - def _on_move_down_button_released(self) -> None: - if self.view.selectedItems(): - src_row = self.view.currentRow() - dst_row = min(self.view.count() - 1, src_row + 1) - self._move_item(src_row, dst_row) - - def _move_item(self, src_row: int, dst_row: int) -> None: - src_item = self.view.takeItem(src_row) - src_item_text = src_item.text() - - self.view.insertItem(dst_row, src_item) - self.items_changed.emit() - - for dst_item in self.view.findItems(src_item_text, QtCore.Qt.MatchExactly): - self.view.setItemSelected(dst_item, True) - self.view.setCurrentRow(self.view.row(dst_item)) - break - - -class ListView(QtWidgets.QListView): - current_row_changed = QtCore.Signal(int) - - def selectionChanged( - self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection - ) -> None: - # Emit last selected row - indexes = selected.indexes() - if indexes: - self.current_row_changed.emit(indexes[-1].row()) - else: - self.current_row_changed.emit(-1) - - -class ItemModelListWidget(BaseItemView): - """list view with filter edit and add and remove buttons.""" - - item_double_clicked = QtCore.Signal(QtCore.QModelIndex) - - def __init__( - self, - model: "BaseConfigItemModel", - model_column: int, - item_flags: QtCore.Qt.ItemFlags = BaseItemView.DEFAULT_ITEM_FLAGS, - item_icon: Optional[QtGui.QIcon] = None, - items_constant: bool = False, - parent: Optional[QtCore.QObject] = None, - ): - """ - :param model: list view model - :param model_column: Model column to get values from - :param item_flags: list item flags - :param item_icon: Optional item icon - :param items_constant: Optionally hide the add and remove - buttons, for implementations where items are - auto-populated. Note that preset support is dependent on - this being False. - """ - self._model = model - self._model.dataChanged.connect(self._on_model_data_changed) - self._model_column = model_column - - list_view = ListView() - list_view.setModel(self._model) - list_view.setModelColumn(self._model_column) - list_view.current_row_changed.connect(self.current_row_changed.emit) - list_view.doubleClicked.connect(self.item_double_clicked.emit) - - has_presets = self._model.has_presets() - - super().__init__( - list_view, - item_flags=item_flags, - item_icon=item_icon, - items_constant=items_constant, - get_presets=None if not has_presets else self._model.get_presets, - presets_only=self._model.requires_presets(), - parent=parent, - ) - - def clear(self) -> None: - row_count = self._model.rowCount() - if row_count: - self._model.removeRows(0, row_count) - - def items(self) -> list[str]: - items = [] - for row in range(self._model.rowCount()): - items.append( - self._model.data( - self._model.index(row, self._model_column), - role=QtCore.Qt.DisplayRole, - ) - ) - return items - - def set_current_item(self, text: str) -> tuple[bool, int]: - indices = self._find_indices(text, hits=1) - if indices: - index = indices[0] - row = index.row() - self.view.setCurrentIndex(index) - self.current_row_changed.emit(row) - return True, row - else: - return False, self.current_row() - - def current_index(self) -> Optional[QtCore.QModelIndex]: - """ - :return: Current model index - """ - return self.view.currentIndex() - - def current_row(self) -> int: - """ - :return: Current list row - """ - return self.current_index().row() - - def set_current_row(self, row: int) -> None: - """ - :param row: Make the specified row current - """ - if row < self._model.rowCount(): - self.view.setCurrentIndex(self._model.index(row, self._model_column)) - - def add_item(self, text: Optional[str] = None) -> None: - item_row = -1 - if self._has_presets and text is not None: - # Try to create preset item - item_row = self._model.add_preset(text) - - if item_row == -1: - item_row = self._model.create_item(text) - - if item_row != -1: - self.set_current_row(item_row) - - def remove_item(self, text: str) -> None: - indices = self._find_indices(text) - if indices: - for index in indices: - self._model.removeRows(index.row(), 1) - - @QtCore.Slot(str) - def _on_filter_text_changed(self, text: str) -> None: - if len(text) < 2: - for row in range(self._model.rowCount()): - self.view.setRowHidden(row, False) - return - - for row in range(self._model.rowCount()): - self.view.setRowHidden(row, True) - - for index in self._find_indices(text, flags=QtCore.Qt.MatchContains): - self.view.setRowHidden(index.row(), False) - - def _on_add_button_released(self) -> None: - self.add_item() - - def _on_remove_button_released(self) -> None: - selection_model = self.view.selectionModel() - for index in sorted( - selection_model.selectedIndexes(), key=lambda i: i.row(), reverse=True - ): - self._model.removeRows(index.row(), 1) - - def _on_move_up_button_released(self) -> None: - current_index = self.view.currentIndex() - name = self._model.data(current_index, QtCore.Qt.DisplayRole) - self._model.move_item_up(name) - - def _on_move_down_button_released(self) -> None: - current_index = self.view.currentIndex() - name = self._model.data(current_index, QtCore.Qt.DisplayRole) - self._model.move_item_down(name) - - @QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex, list) - def _on_model_data_changed( - self, - top_left: QtCore.QModelIndex, - bottom_right: QtCore.QModelIndex, - roles: list[QtCore.Qt.ItemDataRole] = (), - ): - """ - Called when the data for one or more indices in the model - changes. - """ - if top_left.column() == self._model_column: - self.items_changed.emit() - - def _find_indices( - self, - text: str, - hits: int = -1, - flags: QtCore.Qt.MatchFlags = QtCore.Qt.MatchExactly, - ) -> list[QtCore.QModelIndex]: - """ - Search the model for items matching the provided text and - flags. - - :param text: Text to search for in model column - :param hits: Optional maximum number of items to find. Defaults - to find all items. - :param flags: Match flags - :return: list of matching model indices - """ - if not self._model.rowCount(): - return [] - else: - flags |= QtCore.Qt.MatchWrap - return self._model.match( - self._model.index(0, self._model_column), - QtCore.Qt.DisplayRole, - text, - hits=hits, - flags=flags, - ) diff --git a/src/apps/ocioview/ocioview/widgets/log_view.py b/src/apps/ocioview/ocioview/widgets/log_view.py deleted file mode 100644 index 91f79b8f4f..0000000000 --- a/src/apps/ocioview/ocioview/widgets/log_view.py +++ /dev/null @@ -1,91 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Any, Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_BUTTON -from ..style import ( - apply_top_tool_bar_style, - apply_widget_with_top_tool_bar_style, -) -from .text_edit import HtmlView - - -class LogView(QtWidgets.QFrame): - """Base widget for a log/code viewer and toolbar.""" - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - - source_font = QtGui.QFont("Courier") - source_font.setPointSize(10) - - # Widgets - self.html_view = HtmlView() - self.html_view.setFont(source_font) - - tool_bar_stretch = QtWidgets.QFrame() - tool_bar_stretch.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed - ) - - self.tool_bar = QtWidgets.QToolBar() - self.tool_bar.setContentsMargins(0, 0, 0, 0) - self.tool_bar.setIconSize(ICON_SIZE_BUTTON) - self.tool_bar.addWidget(tool_bar_stretch) - - # Layout - tool_bar_layout = QtWidgets.QVBoxLayout() - tool_bar_layout.setContentsMargins(0, 0, 0, 0) - tool_bar_layout.addWidget(self.tool_bar) - - tool_bar_frame = QtWidgets.QFrame() - tool_bar_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - tool_bar_frame.setObjectName("base_log_view__tool_bar_frame") - apply_top_tool_bar_style(tool_bar_frame) - tool_bar_frame.setLayout(tool_bar_layout) - - inner_layout = QtWidgets.QVBoxLayout() - inner_layout.setContentsMargins(0, 0, 0, 0) - inner_layout.setSpacing(1) - inner_layout.addWidget(tool_bar_frame) - inner_layout.addWidget(self.html_view) - - inner_frame = QtWidgets.QFrame() - inner_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - inner_frame.setObjectName("base_log_view__log_inner_frame") - apply_widget_with_top_tool_bar_style(inner_frame) - inner_frame.setLayout(inner_layout) - - outer_layout = QtWidgets.QVBoxLayout() - outer_layout.setContentsMargins(0, 0, 0, 0) - outer_layout.addWidget(inner_frame) - - outer_frame = QtWidgets.QFrame() - outer_frame.setLayout(outer_layout) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(outer_frame) - self.setLayout(layout) - - def __getattr__(self, item: str) -> Any: - """Route all unknown attributes to HtmlView.""" - return getattr(self.html_view, item) - - def prepend_tool_bar_widget(self, widget: QtWidgets.QWidget) -> None: - """ - Insert a widget at the start of the toolbar. - - :param widget: Widget to insert - """ - self.tool_bar.insertWidget(self.tool_bar.actions()[0], widget) - - def append_tool_bar_widget(self, widget: QtWidgets) -> None: - """ - Add a widget at the end of the toolbar. - - :param widget: Widget to append - """ - self.tool_bar.addWidget(widget) diff --git a/src/apps/ocioview/ocioview/widgets/structure.py b/src/apps/ocioview/ocioview/widgets/structure.py deleted file mode 100644 index d4f49b61a1..0000000000 --- a/src/apps/ocioview/ocioview/widgets/structure.py +++ /dev/null @@ -1,233 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from pathlib import Path -from typing import Optional, Union - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_ITEM, ICON_SIZE_TAB, BORDER_COLOR_ROLE -from ..style import apply_top_tool_bar_style -from ..utils import get_icon - - -class DockTitleBar(QtWidgets.QFrame): - """Dock widget title bar widget with icon.""" - - def __init__( - 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) - - self.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.setObjectName("dock_title_bar") - apply_top_tool_bar_style( - self, bg_color_role=None, border_color_role=BORDER_COLOR_ROLE - ) - - # Widgets - 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() - inner_layout.setContentsMargins(4, 5, 4, 5) - inner_layout.setSpacing(5) - 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) - inner_frame.setObjectName("dock_title_bar__inner_frame") - apply_top_tool_bar_style(inner_frame) - inner_frame.setLayout(inner_layout) - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(inner_frame) - - self.setLayout(layout) - - -class TabbedDockWidget(QtWidgets.QDockWidget): - """ - Dockable tab widget with tab icons that are always oriented - upward. - """ - - # Cached tab icon paths - _tab_icons = {} - - def __init__( - 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, widget=corner_widget)) - self.setFeatures( - QtWidgets.QDockWidget.DockWidgetMovable - | QtWidgets.QDockWidget.DockWidgetFloatable - ) - - self._prev_index = 0 - - # Widgets - self.tabs = QtWidgets.QTabWidget() - self.tabs.setIconSize(ICON_SIZE_TAB) - self.setWidget(self.tabs) - - # Connections - self.dockLocationChanged.connect(self._on_dock_location_changed) - self.tabs.currentChanged.connect(self._on_current_changed) - - def add_tab( - self, - widget: QtWidgets.QWidget, - name: str, - icon_or_path: Union[Path, QtGui.QIcon], - tool_tip: Optional[str] = None, - ) -> None: - """ - Add widget as tab with icon and tool tip. - - :param widget: Widget to add as new tab - :param name: Tab name - :param icon_or_path: Icon file path or QIcon instance - :param tool_tip: Optional tab tooltip. If unspecified, a tooltip - will be determined from the icon name. - """ - # Store original icon for rotation adjustments - if isinstance(icon_or_path, Path): - icon = get_icon(icon_or_path) - else: - icon = icon_or_path - - self._tab_icons[id(widget)] = icon - - # Add new tab, with icon oriented upward - tab_pos = self.tabs.tabPosition() - upright_icon = self._rotate_icon(icon, tab_pos) - - tab_idx = self.tabs.addTab(widget, upright_icon, "") - self.tabs.setTabToolTip(tab_idx, tool_tip or name) - - def _rotate_icon( - self, icon: QtGui.QIcon, tab_pos: QtWidgets.QTabWidget.TabPosition - ) -> QtGui.QIcon: - """ - Rotate icon to be oriented upward for the given tab position. - - :param icon: Icon to rotate - :param tab_pos: Tab position to orient icon for - :return: Rotated icon - """ - icon_rot = { - QtWidgets.QTabWidget.East: -90, - QtWidgets.QTabWidget.West: 90, - }.get(tab_pos, 0) - - xform = QtGui.QTransform() - xform.rotate(icon_rot) - - pixmap = icon.pixmap(ICON_SIZE_TAB) - pixmap = pixmap.transformed(xform, QtCore.Qt.SmoothTransformation) - - return QtGui.QIcon(pixmap) - - @QtCore.Slot(QtCore.Qt.DockWidgetArea) - def _on_dock_location_changed( - self, area: QtCore.Qt.DockWidgetArea - ) -> None: - """ - Adjust tab icons to always orient upward on dock area move. - """ - if area == QtCore.Qt.LeftDockWidgetArea: - tab_pos = QtWidgets.QTabWidget.East - else: - tab_pos = QtWidgets.QTabWidget.West - - self.tabs.setTabPosition(tab_pos) - - # Rotate tab icons so they are always oriented upward - for tab_idx in range(self.tabs.count()): - widget = self.tabs.widget(tab_idx) - - # Get previously stored, un-rotated, icon from widget - icon = self._tab_icons.get(id(widget)) - if icon is not None: - upright_icon = self._rotate_icon(icon, tab_pos) - self.tabs.setTabIcon(tab_idx, upright_icon) - - def _on_current_changed(self, index: int) -> None: - prev_widget = self.tabs.widget(self._prev_index) - next_widget = self.tabs.widget(index) - - # If previous and next tabs both have splitters of the same size, make their - # sizes match. - if hasattr(prev_widget, "splitter") and hasattr( - next_widget, "set_splitter_sizes" - ): - next_widget.set_splitter_sizes(prev_widget.splitter.sizes()) - - self._prev_index = index - - -class ExpandingStackedWidget(QtWidgets.QStackedWidget): - """ - Stacked widget that adjusts its height to the current widget, - rather than match the largest widget in the stack. - """ - - def __init__(self, parent: Optional[QtCore.QObject] = None): - super().__init__(parent=parent) - self.currentChanged.connect(self._on_current_changed) - - def addWidget(self, widget: QtWidgets.QWidget) -> None: - """ - :param widget: Widget to add at the back of the stack and make - current. - """ - super().addWidget(widget) - self._on_current_changed(self.currentIndex()) - - @QtCore.Slot(int) - def _on_current_changed(self, index: int) -> None: - """ - Toggle widget size policy to ignore invisible widget sizes. - """ - for i in range(self.count()): - widget = self.widget(i) - if i == index: - widget.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.MinimumExpanding, - ) - else: - widget.setSizePolicy( - QtWidgets.QSizePolicy.Ignored, - QtWidgets.QSizePolicy.Ignored, - ) diff --git a/src/apps/ocioview/ocioview/widgets/table_widget.py b/src/apps/ocioview/ocioview/widgets/table_widget.py deleted file mode 100644 index fc6df1d5cb..0000000000 --- a/src/apps/ocioview/ocioview/widgets/table_widget.py +++ /dev/null @@ -1,378 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Callable, Optional - -from PySide6 import QtCore, QtGui, QtWidgets - -from ..constants import ICON_SIZE_ITEM -from ..utils import SignalsBlocked, next_name -from .item_view import BaseItemView - - -class StringMapTableWidget(BaseItemView): - """ - Two-column table widget with filter edit and add and remove - buttons. - """ - - def __init__( - self, - header_labels: tuple[str, str], - data: Optional[dict] = None, - item_icon: Optional[QtGui.QIcon] = None, - default_key_prefix: str = "", - default_value: str = "", - parent: Optional[QtWidgets.QWidget] = None, - ): - """ - :param header_labels: Labels for each column - :param data: Optional dictionary to pre-populate table from - """ - super().__init__( - item_view=QtWidgets.QTableWidget(), - item_icon=item_icon, - items_movable=False, - parent=parent, - ) - - self._header_labels = header_labels - self._default_key_prefix = default_key_prefix - self._default_value = default_value - - self.view.setColumnCount(2) - self.view.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows) - self.view.horizontalHeader().setStretchLastSection(True) - self.view.setHorizontalHeaderLabels(self._header_labels) - - vertical_header = self.view.verticalHeader() - vertical_header.setDefaultSectionSize(ICON_SIZE_ITEM.height()) - vertical_header.hide() - - self.view.itemChanged.connect(self._on_view_item_changed) - - if data is not None: - self.set_items(data) - - # DataWidgetMapper user property interface - # NOTE: A list of tuples is used here instead of a dictionary, as passing a - # dictionary through DataWidgetMapper results in `None` being received - # by the model. - @QtCore.Property(list, user=True) - def __data(self) -> list[tuple[str, str]]: - return [(k, v) for k, v in self.items().items()] - - @__data.setter - def __data(self, data: list[tuple[str, str]]) -> None: - with SignalsBlocked(self, self.view): - self.set_items({k: v for k, v in data}) - - def clear(self) -> None: - self.view.setRowCount(0) - - def items(self) -> dict[str, str]: - data = {} - for row in range(self.view.rowCount()): - key_item = self.view.item(row, 0) - value_item = self.view.item(row, 1) - - # Key is required, but value defaults to "" - if key_item is not None: - key = key_item.text() - if key: - if value_item is not None: - value = value_item.text() - else: - value = "" - - data[key] = value - return data - - def set_items(self, data: dict[str, str]) -> None: - """ - Reset table with the provided data. - - :param data: Map key, value pairs - """ - # Clear table without resetting horizontal header items - self.view.setRowCount(0) - self.view.setRowCount(len(data)) - - for row, (key, value) in enumerate(data.items()): - key_item = QtWidgets.QTableWidgetItem(str(key)) - if self._item_icon is not None: - key_item.setIcon(self._item_icon) - self.view.setItem(row, 0, key_item) - value_item = QtWidgets.QTableWidgetItem(str(value)) - self.view.setItem(row, 1, value_item) - - def add_item(self, key: Optional[str] = None, value: Optional[str] = None) -> None: - row = self.view.rowCount() - self.view.setRowCount(row + 1) - - key_item = QtWidgets.QTableWidgetItem( - str( - key - or ( - "" - if not self._default_key_prefix - else next_name(self._default_key_prefix, list(self.items().keys())) - ) - ) - ) - if self._item_icon is not None: - key_item.setIcon(self._item_icon) - - value_item = QtWidgets.QTableWidgetItem(str(value or self._default_value)) - - # Don't emit items changed signal until both key and value have been set, since - # some underlying models will invalidate rows with only key or value set. - with SignalsBlocked(self): - self.view.setItem(row, 0, key_item) - self.view.setItem(row, 1, value_item) - - self.items_changed.emit() - - def remove_item(self, text: str) -> None: - remove_rows = set() - - for item in self.view.findItems(text, QtCore.Qt.MatchExactly): - remove_rows.add(item.row()) - - for row in sorted(remove_rows, reverse=True): - self.view.removeRow(row) - - # Removing items doesn't emit the builtin itemChanged signal - self.items_changed.emit() - - def set_current_item(self, text: str) -> tuple[bool, int]: - items = self.view.findItems(text, QtCore.Qt.MatchExactly) - if items: - item = items[0] - row = item.row() - self.view.selectRow(row) - return True, row - else: - return False, self.view.currentIndex().row() - - @QtCore.Slot(str) - def _on_filter_text_changed(self, text: str) -> None: - if len(text) < 2: - for row in range(self.view.rowCount()): - self.view.setRowHidden(row, False) - return - - for row in range(self.view.rowCount()): - self.view.setRowHidden(row, True) - - for item in self.view.findItems(text, QtCore.Qt.MatchContains): - self.view.setRowHidden(item.row(), False) - - def _on_add_button_released(self) -> None: - self.add_item() - - def _on_remove_button_released(self) -> None: - remove_rows = set() - - for item in self.view.selectedItems(): - remove_rows.add(item.row()) - - for row in sorted(remove_rows, reverse=True): - self.view.removeRow(row) - - # Removing items doesn't emit the builtin itemChanged signal - self.items_changed.emit() - - def _on_view_item_changed(self, *args, **kwargs) -> None: - """Notify watchers of item changes""" - self.items_changed.emit() - - -class ItemModelTableWidget(BaseItemView): - """Table view with filter edit and add and remove buttons.""" - - def __init__( - self, - model: QtCore.QAbstractTableModel, - get_presets: Optional[Callable] = None, - presets_only: bool = False, - parent: Optional[QtWidgets.QWidget] = None, - ): - """ - :param model: Table model - """ - super().__init__( - item_view=QtWidgets.QTableView(), - items_movable=False, - get_presets=get_presets, - presets_only=presets_only, - parent=parent, - ) - - self._model = model - self._model.dataChanged.connect(self._on_view_item_changed) - self._model.item_added.connect(self._on_item_added) - - self.view.setModel(self._model) - self.view.setHorizontalScrollMode(QtWidgets.QTableWidget.ScrollPerPixel) - self.view.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows) - self.view.selectionModel().currentRowChanged.connect( - lambda current, previous: self.current_row_changed.emit(current.row()) - ) - - horizontal_header = self.view.horizontalHeader() - horizontal_header.setStretchLastSection(True) - horizontal_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - - vertical_header = self.view.verticalHeader() - vertical_header.setDefaultSectionSize(ICON_SIZE_ITEM.height()) - vertical_header.hide() - - def clear(self) -> None: - row_count = self._model.rowCount() - if row_count: - self._model.removeRows(0, row_count) - - def items(self) -> list[list[str]]: - items = [] - column_count = self._model.columnCount() - - for row in range(self._model.rowCount()): - item_data = [] - for column in range(column_count): - item_data.append( - self._model.data( - self._model.index(row, column), - role=QtCore.Qt.DisplayRole, - ) - ) - items.append(item_data) - - return items - - def add_item(self, text: Optional[str] = None) -> None: - item_row = -1 - if self._has_presets and text is not None: - # Try to create preset item - item_row = self._model.add_preset(text) - - if item_row == -1: - # Let model create a default item - if self._model.insertRows(0, 1): - item_row = 0 - - self.set_current_row(item_row) - - def remove_item(self, text: str) -> None: - remove_rows = set() - - for index in self._find_indices(text, flags=QtCore.Qt.MatchExactly): - remove_rows.add(index.row()) - - for row in sorted(remove_rows, reverse=True): - self._model.removeRows(row, 1) - - # Removing items doesn't emit the builtin itemChanged signal - self.items_changed.emit() - - def set_current_item(self, text: str) -> tuple[bool, int]: - indices = self._find_indices(text, hits=1, flags=QtCore.Qt.MatchExactly) - if indices: - index = indices[0] - row = index.row() - self.view.selectRow(row) - return True, row - else: - return False, self.view.currentIndex().row() - - def set_current_row(self, row: int) -> None: - """ - :param row: Make the specified row current - """ - self.view.selectRow(row) - - @QtCore.Slot(QtCore.QModelIndex) - def _on_item_added(self, name: str) -> None: - """Set the most recently added item as current/selected.""" - for index in self._find_indices(name): - self.view.setCurrentIndex(index) - - @QtCore.Slot(str) - def _on_filter_text_changed(self, text: str) -> None: - if len(text) < 2: - for row in range(self._model.rowCount()): - self.view.setRowHidden(row, False) - return - - for row in range(self._model.rowCount()): - self.view.setRowHidden(row, True) - - for index in self._find_indices(text, flags=QtCore.Qt.MatchContains): - self.view.setRowHidden(index.row(), False) - - def _on_add_button_released(self) -> None: - self.add_item() - - def _on_remove_button_released(self) -> None: - remove_rows = set() - - for index in self.view.selectedIndexes(): - remove_rows.add(index.row()) - - for row in sorted(remove_rows, reverse=True): - self._model.removeRows(row, 1) - - # Removing items doesn't emit the builtin itemChanged signal - self.items_changed.emit() - - def _on_move_up_button_released(self) -> None: - current_index = self.view.currentIndex() - parent = current_index.parent() - row = current_index.row() - - self.view.model().moveRow(parent, row, parent, row - 1) - - def _on_move_down_button_released(self) -> None: - current_index = self.view.currentIndex() - parent = current_index.parent() - row = current_index.row() - - self.view.model().moveRow(parent, row, parent, row + 1) - - def _on_view_item_changed(self, *args, **kwargs) -> None: - """Notify watchers of item changes""" - self.items_changed.emit() - - def _find_indices( - self, - text: str, - hits: int = -1, - flags: QtCore.Qt.MatchFlags = QtCore.Qt.MatchFixedString | QtCore.Qt.MatchWrap, - ) -> list[QtCore.QModelIndex]: - """ - Search the model for items matching the provided text and - flags. - - :param text: Text to search for in model column - :param hits: Optional maximum number of items to find. Defaults - to find all items. - :param flags: Match flags - :return: list of matching model indices - """ - if not self._model.rowCount(): - return [] - else: - flags |= QtCore.Qt.MatchWrap - - rows = set() - for column in range(self._model.columnCount()): - indices = self._model.match( - self._model.index(0, column), - QtCore.Qt.DisplayRole, - text, - hits=hits, - flags=flags, - ) - rows.update(index.row() for index in indices) - - return [self._model.index(row, 0) for row in sorted(rows)] diff --git a/src/apps/ocioview/ocioview/widgets/text_edit.py b/src/apps/ocioview/ocioview/widgets/text_edit.py deleted file mode 100644 index d654215ec7..0000000000 --- a/src/apps/ocioview/ocioview/widgets/text_edit.py +++ /dev/null @@ -1,72 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright Contributors to the OpenColorIO Project. - -from typing import Optional - -from PySide6 import QtCore, QtWidgets - -from ..utils import SignalsBlocked - - -class TextEdit(QtWidgets.QPlainTextEdit): - def __init__( - self, text: Optional[str] = None, parent: Optional[QtCore.QObject] = None - ): - super().__init__(parent=parent) - self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) - - if text is not None: - self.set_value(text) - - # DataWidgetMapper user property interface - @QtCore.Property(str, user=True) - def __data(self) -> str: - return self.value() - - @__data.setter - def __data(self, data: str) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> str: - return self.toPlainText() - - def set_value(self, value: str) -> None: - self.setPlainText(value) - - def reset(self) -> None: - self.clear() - - -class HtmlView(QtWidgets.QTextEdit): - def __init__( - self, text: Optional[str] = None, parent: Optional[QtCore.QObject] = None - ): - super().__init__(parent=parent) - self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) - self.setReadOnly(True) - self.setUndoRedoEnabled(False) - - if text is not None: - self.set_value(text) - - # DataWidgetMapper user property interface - @QtCore.Property(str, user=True) - def __data(self) -> str: - return self.value() - - @__data.setter - def __data(self, data: str) -> None: - with SignalsBlocked(self): - self.set_value(data) - - # Common public interface - def value(self) -> str: - return self.toHtml() - - def set_value(self, value: str) -> None: - self.setHtml(value) - - def reset(self) -> None: - self.clear() diff --git a/src/apps/ocioview/pyproject.toml b/src/apps/ocioview/pyproject.toml deleted file mode 100644 index 10ec8d460f..0000000000 --- a/src/apps/ocioview/pyproject.toml +++ /dev/null @@ -1,203 +0,0 @@ -[project] -name = "ocioview" -version = "0.1.0" -description = "OpenColorIO config visual editor application" -requires-python = ">=3.10,<3.14" -authors = [ - { name = "Contributors to the OpenColorIO Project" } -] -maintainers = [ - { name = "Contributors to the OpenColorIO Project" } -] -license = { text = "BSD-3-Clause" } -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering", - "Topic :: Software Development", -] -dependencies = [ - "colour-science>=0.4.5,<0.5", - "colour-visuals", - "imageio>=2,< 3", - "networkx>=3,<4", - "numpy>=1.24,<3", - "opencolorio>=2,<3", - "pygfx>=0.1,<0.3", - "pygments", - "pyopengl", - "pyside6", - "qtawesome", - "scipy>=1.10,<2", -] - -[tool.uv.sources] -"colour-visuals" = { git = "https://github.com/colour-science/colour-visuals.git" } - -[project.optional-dependencies] -docs = [ - "restructuredtext-lint", - "sphinx", -] - -[tool.uv] -package = true -dev-dependencies = [ - "black", - "coverage", - "coveralls", - "hatch", - "invoke", - "jupyter", - "pre-commit", - "pyright", - "pytest", - "pytest-cov", - "pytest-xdist", - "toml", - "twine", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = [ "ocioview" ] - -[tool.black] -line-length = 79 -exclude = ''' -/( - \.git - | build - | dist -)/ -''' - -[tool.flynt] -line_length = 999 - -[tool.pyright] -reportMissingImports = false -reportMissingModuleSource = false -reportUnboundVariable = false -reportUnnecessaryCast = true -reportUnnecessaryTypeIgnoreComment = true -reportUnsupportedDunderAll = false -reportUnusedExpression = false - -[tool.pytest.ini_options] -addopts = "-n auto --dist=loadscope --durations=5" -filterwarnings = [ - "ignore::RuntimeWarning", - "ignore::pytest.PytestCollectionWarning", - "ignore:Implicit None on return values is deprecated:DeprecationWarning", - "ignore:Jupyter is migrating its paths:DeprecationWarning", - "ignore:the imp module is deprecated:DeprecationWarning", - "ignore:Method Nelder-Mead does not use gradient information:RuntimeWarning", - "ignore:More than 20 figures have been opened:RuntimeWarning", - "ignore:divide by zero encountered:RuntimeWarning", - "ignore:invalid value encountered in:RuntimeWarning", - "ignore:overflow encountered in:RuntimeWarning", - "ignore:Matplotlib is currently using agg:UserWarning", - "ignore:override the edgecolor or facecolor properties:UserWarning", -] - -[tool.ruff] -target-version = "py310" -line-length = 88 -select = [ - "A", # flake8-builtins - "ARG", # flake8-unused-arguments - # "ANN", # flake8-annotations - "B", # flake8-bugbear - # "BLE", # flake8-blind-except - "C4", # flake8-comprehensions - # "C90", # mccabe - # "COM", # flake8-commas - "DTZ", # flake8-datetimez - "D", # pydocstyle - "E", # pydocstyle - # "ERA", # eradicate - # "EM", # flake8-errmsg - "EXE", # flake8-executable - "F", # flake8 - # "FBT", # flake8-boolean-trap - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "ISC", # flake8-implicit-str-concat - "N", # pep8-naming - # "PD", # pandas-vet - "PIE", # flake8-pie - "PGH", # pygrep-hooks - "PL", # pylint - # "PT", # flake8-pytest-style - # "PTH", # flake8-use-pathlib [Enable] - "Q", # flake8-quotes - "RET", # flake8-return - "RUF", # Ruff - "S", # flake8-bandit - "SIM", # flake8-simplify - "T10", # flake8-debugger - "T20", # flake8-print - # "TCH", # flake8-type-checking - "TID", # flake8-tidy-imports - "TRY", # tryceratops - "UP", # pyupgrade - "W", # pydocstyle - "YTT", # flake8-2020 -] -ignore = [ - "B008", - "B905", - "D104", - "D200", - "D202", - "D205", - "D301", - "D400", - "I001", - "N801", - "N802", - "N803", - "N806", - "N813", - "N815", - "N816", - "PGH003", - "PIE804", - "PLE0605", - "PLR0911", - "PLR0912", - "PLR0913", - "PLR0915", - "PLR2004", - "RET504", - "RET505", - "RET506", - "RET507", - "RET508", - "TRY003", - "TRY300", - "UP038", -] -typing-modules = ["colour.hints"] -fixable = ["B", "C", "E", "F", "PIE", "RUF", "SIM", "UP", "W"] - -[tool.ruff.pydocstyle] -convention = "numpy" - -[tool.ruff.per-file-ignores] -"docs/*" = ["INP"] - -[tool.ruff.format] -docstring-code-format = true diff --git a/src/apps/ocioview/requirements.txt b/src/apps/ocioview/requirements.txt deleted file mode 100644 index ec6b8bca1e..0000000000 --- a/src/apps/ocioview/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -colour-science -colour-visuals @ git+https://github.com/colour-science/colour-visuals.git -imageio -networkx -numpy -OpenColorIO -pygfx -pygments -PyOpenGL -PySide6 -QtAwesome -scipy diff --git a/src/apps/ocioview/tests/conftest.py b/src/apps/ocioview/tests/conftest.py deleted file mode 100644 index 94240b71ac..0000000000 --- a/src/apps/ocioview/tests/conftest.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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 deleted file mode 100644 index 596265cf61..0000000000 --- a/src/apps/ocioview/tests/items/test_color_space_edit.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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 - )